Raising & Custom Exceptions
Your own code should raise exceptions too — when it receives invalid input or reaches a state it can't handle. Clear exceptions, including custom exception classes for your application, make failures easy to understand and to handle correctly higher up.
raise
def withdraw(balance, amount): if amount <= 0: raise ValueError("Amount must be positive") if amount > balance: raise ValueError(f"Insufficient funds: balance {balance}, requested {amount}") return balance - amount print(withdraw(100, 30)) for amount in (-5, 500): try: withdraw(100, amount) except ValueError as err: print("Error:", err)
70 Error: Amount must be positive Error: Insufficient funds: balance 100, requested 500
Raise an exception instance with a helpful message: what went wrong, and ideally the values involved. Choose the built-in type that fits — ValueError for bad values, TypeError for wrong types, KeyError/LookupError for missing items.
Re-raising
Sometimes you want to react to an exception (log it, clean up) but still let it propagate. A bare raise inside except re-raises the current exception unchanged:
def load(path): try: raise FileNotFoundError(path) except FileNotFoundError: print(f"log: could not open {path}") raise try: load("settings.toml") except FileNotFoundError as err: print("caller handles:", err)
log: could not open settings.toml caller handles: settings.toml
Custom exception classes
Defining your own exception classes lets callers catch your errors specifically, and carry extra information:
class AppError(Exception): """Base class for all errors raised by this application.""" class ValidationError(AppError): def __init__(self, field, message): super().__init__(f"{field}: {message}") self.field = field class PaymentDeclined(AppError): def __init__(self, reason, retryable=False): super().__init__(f"payment declined ({reason})") self.retryable = retryable def checkout(order): if not order.get("email"): raise ValidationError("email", "is required") if order["total"] > 1000: raise PaymentDeclined("limit exceeded", retryable=False) return "paid" for order in [{"total": 10}, {"email": "a@x.com", "total": 5000}, {"email": "a@x.com", "total": 50}]: try: print(checkout(order)) except ValidationError as err: print(f"Please fix the {err.field} field — {err}") except AppError as err: print(f"Something went wrong: {err}")
Please fix the email field — email: is required Something went wrong: payment declined (limit exceeded) paid
A common structure is one base class for the application (AppError) with specific subclasses. Callers can catch a specific error or all of yours at once, while genuine bugs (like TypeError) still surface.
Chaining: raise … from …
When you convert a low-level exception into a higher-level one, chain them so the original cause isn't lost:
import json class ConfigError(Exception): pass def load_config(text): try: return json.loads(text) except json.JSONDecodeError as err: raise ConfigError("config file is not valid JSON") from err try: load_config("{bad json") except ConfigError as err: print(err) print("caused by:", type(err.__cause__).__name__)
config file is not valid JSON caused by: JSONDecodeError
In a full traceback, Python prints both exceptions with "The above exception was the direct cause of the following exception". Use from None to deliberately hide the original when it adds nothing.
Adding notes
def process(rows): for i, row in enumerate(rows, 1): try: int(row) except ValueError as err: err.add_note(f"while processing row {i}") raise try: process(["1", "2", "x"]) except ValueError as err: print(err) print(err.__notes__)
invalid literal for int() with base 10: 'x' ['while processing row 3']
add_note() attaches extra context that appears at the end of the traceback.
assert: checking your own assumptions
def average(values): assert len(values) > 0, "average() needs at least one value" return sum(values) / len(values) print(average([2, 4])) average([])
3.0
Traceback (most recent call last):
File "main.py", line 6, in <module>
average([])
~~~~~~~^^^^
File "main.py", line 2, in average
assert len(values) > 0, "average() needs at least one value"
^^^^^^^^^^^^^^^
AssertionError: average() needs at least one valueassert is for catching programming mistakes during development, not for validating user input: assertions are removed entirely when Python runs with the -O optimisation flag. Raise a proper exception for anything that can happen in normal use.
Guidelines
- Fail early and loudly: validate inputs at the start of a function.
- Write messages for the person who'll read the traceback — include the bad value.
- Don't use exceptions for ordinary control flow when a simple
ifis clearer. - Document what your functions raise in their docstrings.