raise and Custom Exceptions
raise signals a failure your own code has detected. A custom exception class gives that failure a name callers can catch precisely.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
raise
def set_age(age):
if not isinstance(age, int):
raise TypeError("age must be a whole number")
if age < 0:
raise ValueError("age cannot be negative")
if age > 150:
raise ValueError(f"age {age} is not plausible")
return age
print(set_age(27))
# set_age(-5) # ValueError: age cannot be negativeRaising stops the function immediately and hands the failure to the caller. It is how a function refuses to continue with input it cannot honour.
Choosing the type
| Situation | Raise |
|---|---|
| Wrong type of argument | TypeError |
| Right type, unacceptable value | ValueError |
| A key or index is absent | KeyError / IndexError |
| The object is in the wrong state for this call | RuntimeError |
| A subclass has not implemented this | NotImplementedError |
| The operation is not supported at all | TypeError |
Write a message that helps
raise ValueError("invalid") # useless
raise ValueError("invalid input") # barely better
raise ValueError(f"expected a positive integer, got {age!r}") # usefulInclude what was expected, what arrived, and use !r so that "5" and 5 look different in the message.
Re-raising
def process(path):
try:
with open(path) as handle:
return handle.read()
except PermissionError:
print(f"audit: permission denied for {path}")
raise # bare raise keeps the original tracebacktry:
int("abc")
except ValueError as error:
raise ValueError("could not parse the configuration") from errorA bare raise re-raises the current exception unchanged. raise X from error replaces it while recording the cause. Both preserve information; raise X on its own inside a handler loses the connection and makes debugging harder.
Custom exceptions
class ValidationError(Exception):
"""Raised when submitted data fails validation."""
def register(username):
if len(username) < 3:
raise ValidationError("username must be at least 3 characters")
return username
try:
register("ab")
except ValidationError as error:
print("rejected:", error)A class with a docstring and nothing else is a perfectly good exception. Inherit from Exception, never from BaseException.
Why bother
# Without a custom type, the caller cannot tell these apart
def charge(amount):
if amount <= 0:
raise ValueError("amount must be positive")
if amount > balance:
raise ValueError("insufficient funds") # same type, different meaning
# With custom types, the caller can respond differently to each
class PaymentError(Exception):
"""Base class for every payment failure."""
class InvalidAmount(PaymentError):
"""The amount is not a usable value."""
class InsufficientFunds(PaymentError):
"""The account does not hold enough to cover this."""
def charge(amount, balance):
if amount <= 0:
raise InvalidAmount(f"amount must be positive, got {amount}")
if amount > balance:
raise InsufficientFunds(f"need {amount}, have {balance}")
return balance - amount
try:
charge(500, 100)
except InsufficientFunds as error:
print("top up required:", error)
except PaymentError as error:
print("payment problem:", error)Always define a base class for your module
Exception
+-- PaymentError <-- callers can catch everything from this module
+-- InvalidAmount
+-- InsufficientFunds
+-- CardDeclinedOne base class per library or module lets a caller write except PaymentError: and be sure of covering your whole surface, without resorting to except Exception.
Carrying extra data
class ValidationError(Exception):
"""Raised when a field fails validation."""
def __init__(self, field, value, reason):
self.field = field
self.value = value
self.reason = reason
super().__init__(f"{field}={value!r}: {reason}")
try:
raise ValidationError("age", "-5", "must be positive")
except ValidationError as error:
print(error) # age='-5': must be positive
print(error.field) # age
print(error.value) # -5
print(error.reason) # must be positiveCalling super().__init__(message) is what makes str(error) useful. Store the individual pieces as attributes so a handler can act on them rather than parsing the message text.
class RetryableError(Exception):
def __init__(self, message, retry_after=30):
super().__init__(message)
self.retry_after = retry_after
try:
raise RetryableError("service busy", retry_after=60)
except RetryableError as error:
print(f"{error}; try again in {error.retry_after}s")Collecting several failures
class ValidationError(Exception):
pass
def validate(record):
problems = []
if not record.get("name"):
problems.append("name is required")
if not isinstance(record.get("age"), int):
problems.append("age must be a whole number")
elif record["age"] < 0:
problems.append("age cannot be negative")
if "@" not in record.get("email", ""):
problems.append("email is not valid")
if problems:
raise ValidationError("; ".join(problems))
return record
try:
validate({"age": "x", "email": "nope"})
except ValidationError as error:
for problem in str(error).split("; "):
print("-", problem)Reporting every problem at once is far more useful to a user than stopping at the first one. Note the elif: the range check only runs when the type check passed.
assert is not error handling
def average(values):
assert values, "values must not be empty" # a developer assumption
return sum(values) / len(values)
def average_safe(values):
if not values:
raise ValueError("values must not be empty") # a real check
return sum(values) / len(values)Assertions are removed entirely when Python runs with the-Oflag. Useassertto state something you believe can never happen; useraiseto validate anything that comes from outside your program.
A worked example
class ConfigError(Exception):
"""Base class for configuration problems."""
class MissingSetting(ConfigError):
def __init__(self, key):
self.key = key
super().__init__(f"required setting {key!r} is missing")
class InvalidSetting(ConfigError):
def __init__(self, key, value, expected):
self.key = key
self.value = value
super().__init__(f"setting {key!r} is {value!r}, expected {expected}")
def load_port(config):
if "port" not in config:
raise MissingSetting("port")
try:
port = int(config["port"])
except (TypeError, ValueError) as error:
raise InvalidSetting("port", config["port"], "a whole number") from error
if not 1 <= port <= 65535:
raise InvalidSetting("port", port, "a number between 1 and 65535")
return port
for config in [{}, {"port": "abc"}, {"port": 99999}, {"port": "8080"}]:
try:
print("port:", load_port(config))
except ConfigError as error:
print("config error:", error)Common mistakes
- Raising a bare
Exceptioninstead of a specific or custom type. - Inheriting a custom exception from
BaseException. - Overriding
__init__and forgetting to callsuper().__init__, so the message is lost. - Writing
raise ValueErrorwithout brackets - legal, but it gives no message. - Using
assertto validate user input. - Raising inside a loop and abandoning the remaining work when collecting failures would be better.
Best practices
- Give every module or package one base exception class of its own.
- Name exception classes ending in
Error. - Put the values that caused the failure in the message, and also on the object as attributes.
- Use
raise ... from errorwhen translating between layers. - Validate with
raise; assert only your own internal assumptions.
Practice
- Write a small exception hierarchy for a library system: a base class plus three specific errors.
- Write a custom exception carrying a field name and an invalid value as attributes.
- Validate a record and report every problem in one exception rather than the first.
- Show why
raise ValueError("x") from errorproduces a better traceback thanraise ValueError("x")inside a handler. - Explain when
assertis appropriate and give one case where using it would be a security problem.
Conclusion
Raise when your code detects something it cannot honour, choose the type that describes the failure, and write a message containing the offending value. Give your module its own exception base class, and callers will be able to handle your failures precisely instead of catching everything.