Custom Exceptions
Python’s built-in exceptions cover generic problems well — a bad value, a missing key, a wrong type — but they say nothing about the meaning of a failure within your own application. A ValueError raised deep inside an order-processing function does not tell a caller whether the problem was a bad email address, an out-of-stock item, or an expired coupon. Custom exceptions let you name failures in the vocabulary of your domain, and let callers catch exactly the failures they know how to handle.
Defining a Custom Exception
A custom exception is just a class that inherits, directly or indirectly, from Exception. The simplest version needs no code at all — inheriting is enough to make it raisable, catchable, and printable like any built-in exception.
class InsufficientFundsError(Exception):
"""Raised when an account does not have enough balance for a withdrawal."""
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(f"cannot withdraw {amount}, balance is {balance}")
return balance - amount
withdraw(50, 200)Traceback (most recent call last):
File "bank.py", line 8, in <module>
withdraw(50, 200)
File "bank.py", line 5, in withdraw
raise InsufficientFundsError(f"cannot withdraw {amount}, balance is {balance}")
InsufficientFundsError: cannot withdraw 200, balance is 50Adding Custom Attributes
Overriding __init__ lets a custom exception carry structured data alongside its message — not just text for a human to read, but values a try/except block can inspect programmatically to decide what to do next.
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
self.shortfall = amount - balance
super().__init__(
f"cannot withdraw {amount}, balance is {balance} "
f"(short by {self.shortfall})"
)
try:
withdraw(50, 200)
except InsufficientFundsError as e:
print(f"Declined. You need {e.shortfall} more.")Building an Exception Hierarchy
For anything beyond a trivial script, define one base exception for your application and derive more specific exceptions from it. This mirrors how Python’s own built-in exceptions are organized, and gives callers a choice: catch broadly at the base level, or narrowly at a specific subtype.
class AppError(Exception):
"""Base class for all application-specific errors."""
class ValidationError(AppError):
"""Raised when input data fails validation rules."""
def __init__(self, field, reason):
self.field = field
self.reason = reason
super().__init__(f"invalid value for '{field}': {reason}")
class NotFoundError(AppError):
"""Raised when a requested resource does not exist."""
def __init__(self, resource, identifier):
self.resource = resource
self.identifier = identifier
super().__init__(f"{resource} '{identifier}' was not found")
def get_user(users, user_id):
if not isinstance(user_id, int):
raise ValidationError("user_id", "must be an integer")
if user_id not in users:
raise NotFoundError("user", user_id)
return users[user_id]Catching at the Right Level
Because ValidationError and NotFoundError both inherit from AppError, a caller can catch each one individually to react differently, or catch AppError alone to handle “anything this application deems an expected failure” in one place — while unrelated bugs (a TypeError from a real programming mistake elsewhere) still propagate normally instead of being silently absorbed.
def handle_request(users, user_id):
try:
user = get_user(users, user_id)
return {"status": "ok", "user": user}
except ValidationError as e:
return {"status": "error", "field": e.field, "message": str(e)}
except NotFoundError as e:
return {"status": "error", "resource": e.resource, "message": str(e)}
except AppError as e:
# Fallback for any other application-defined error
return {"status": "error", "message": str(e)}Why Custom Exceptions Improve API Design
A library or module that only ever raises generic ValueError and Exception forces every caller to either catch broadly (and risk hiding unrelated bugs) or parse error message strings to figure out what actually happened — a brittle, unmaintainable pattern. A well-designed exception hierarchy solves this at the type level instead.
Callers can catch narrowly — handling
NotFoundErrordifferently fromValidationError— without string-matching messages.Custom attributes (
field,resource,shortfall) carry structured data a handler can act on programmatically, not just prose for a human.A shared base class like
AppErrorlets code that only cares about "did my application fail" catch one type, while bugs and third-party errors still surface normally.The exception names themselves document the failure modes of your API — a big improvement over discovering them by reading source code.