Introduction
Knowing how to raise, re-raise, chain, and define custom exceptions is essential for writing libraries and applications that communicate errors clearly. This lesson covers the raise statement, exception chaining, custom exception hierarchies, and the role of assert.
Key Concepts
raise: Throws an exception, stopping normal execution.- Re-raising (
raisewith no argument): Propagates the currently handled exception after logging or partial handling. - Exception chaining (
from): Links a new exception to its root cause via__cause__. - Custom exceptions: Application-specific exception classes that inherit from
Exception. assert: A debugging aid that raisesAssertionErrorwhen a condition is false (can be disabled with-O).
Real World Context
Well-designed exception hierarchies let callers decide how granularly to handle errors. A library might raise DatabaseError as a base, with ConnectionError and QueryError as specifics. Callers who want a blanket handler catch DatabaseError; those who need precision catch the subclass. Exception chaining preserves the full error trail, which is invaluable for debugging issues in production.
Deep Dive
The raise Statement
pythondef divide(a, b): if b == 0: raise ValueError("Division by zero is not allowed") return a / b def validate_age(age): if age < 0: raise ValueError("Age cannot be negative") if age > 150: raise ValueError("Age seems unrealistic")
Re-raising Exceptions
pythontry: process() except ValueError: log("Processing failed") raise # Re-raise the same exception
Exception Chaining
pythontry: config = load_config() except FileNotFoundError as e: raise ConfigError("Config file missing") from e # The original exception is preserved in __cause__
Custom Exceptions
pythonclass ValidationError(Exception): """Raised when validation fails.""" def __init__(self, field, message): self.field = field self.message = message super().__init__(f"{field}: {message}") class DatabaseError(Exception): """Base class for database errors.""" pass class ConnectionError(DatabaseError): pass class QueryError(DatabaseError): pass
assert Statements
pythondef calculate_discount(price, percent): assert 0 <= percent <= 100, "Percent must be 0-100" assert price > 0, "Price must be positive" return price * (1 - percent / 100) # Note: asserts can be disabled with -O flag
Common Pitfalls
- Using
assertfor input validation -- Assertions are disabled when Python runs with-O(optimize). Never useassertto validate user input or function arguments in production; useraise ValueErrorinstead. - Forgetting
from ewhen chaining exceptions -- Withoutfrom e, the original traceback is lost, making it harder to find the root cause. Always chain withfromwhen wrapping exceptions. - Creating custom exceptions that do not call
super().__init__()-- Skipping the parent initializer can break pickling and string representation. Always callsuper().__init__(message).
Best Practices
- Build a custom exception hierarchy for your library -- A single base exception (e.g.,
AppError) lets callers catch everything from your library with one clause while still being able to handle specifics. - Always use
raisewithout arguments to re-raise --raisealone preserves the original traceback.raise eresets it, losing context.
Summary
- Use
raiseto signal errors explicitly with clear messages. - Re-raise with bare
raiseto preserve the full traceback. - Chain exceptions with
fromto link new errors to their root cause. - Build custom exception hierarchies for libraries and applications.
- Never use
assertfor production input validation -- it can be disabled.
Code Examples
python
# Best practice: custom exception hierarchy
class AppError(Exception):
"""Base exception for our application."""
pass
class UserNotFoundError(AppError):
def __init__(self, user_id):
self.user_id = user_id
super().__init__(f"User {user_id} not found")
class PermissionDeniedError(AppError):
def __init__(self, action, resource):
super().__init__(f"Cannot {action} on {resource}")