Custom Exception Classes — Designing Your Own Error Types
~18 min · custom-exception, exception-class, raise
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
Why custom exceptions matter
The built-in exceptions are general-purpose: ValueError, TypeError, RuntimeError. They tell you a category of problem but not the specific failure your code experienced. A custom exception class — InsufficientFunds, RateLimitExceeded, InvalidUserToken — communicates intent and lets callers handle exactly the cases they care about.
Inheriting from the right base
Always inherit from Exception, not BaseException. BaseException is the root for things like SystemExit and KeyboardInterrupt that shouldn't be caught by general handlers. Your custom exceptions should slot into the normal "recoverable error" tree under Exception.
Adding context — attributes on the exception
The simplest custom exception is just class MyError(Exception): pass. The next step up: a custom __init__ that captures relevant data — InsufficientFunds(account_id, requested, available). Now exception handlers can inspect e.account_id and respond intelligently.
Exception hierarchies — when you have many related errors
If your library has a family of related errors, build a small hierarchy. class ApiError(Exception): pass as the root, then RateLimitExceeded(ApiError), AuthenticationFailed(ApiError). Callers can catch ApiError for "anything from this library" or specific subclasses for fine-grained handling.
raise from — chaining causes
When one exception leads to another — you caught a low-level FileNotFoundError and want to raise a higher-level ConfigMissing — use raise ConfigMissing(...) from original_exc. Python will print both, with the chain shown. The __cause__ attribute on the new exception points to the original.
Code
Minimal custom exception·python
class InsufficientFunds(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFunds(f"asked for {amount}, balance {balance}")
return balance - amount
try:
withdraw(100, 200)
except InsufficientFunds as e:
print("caught:", e)
Custom exception with structured attributes·python
class InsufficientFunds(Exception):
def __init__(self, account_id, requested, available):
super().__init__(
f"account {account_id}: requested {requested}, available {available}"
)
self.account_id = account_id
self.requested = requested
self.available = available
try:
raise InsufficientFunds("acc-42", 200, 100)
except InsufficientFunds as e:
print("message:", e)
print("shortfall:", e.requested - e.available)
# handlers can use the structured fields
if e.account_id.startswith("acc-"):
print("this is a regular account")
class ApiError(Exception):
"""Base for all API client errors."""
class RateLimitExceeded(ApiError):
pass
class AuthenticationFailed(ApiError):
pass
class ServerError(ApiError):
pass
def call_api(token):
if not token:
raise AuthenticationFailed("no token")
if token == "limited":
raise RateLimitExceeded("slow down")
raise ServerError("500 internal")
# Caller can catch broadly
try:
call_api(None)
except ApiError as e:
print("some API problem:", type(e).__name__, e)
# Or narrowly
try:
call_api("limited")
except RateLimitExceeded:
print("backing off")
except ApiError as e:
print("other api error:", e)
raise from — chaining the cause·python
class ConfigError(Exception):
pass
def load_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError as e:
raise ConfigError(f"config not found at {path}") from e
try:
load_config("/nonexistent.yaml")
except ConfigError as e:
print("caught:", e)
print("caused by:", e.__cause__)
# With `raise X from Y`, traceback prints:
# FileNotFoundError: ...
# The above exception was the direct cause of the following exception:
# ConfigError: ...
Build an exception hierarchy for a fictional payment module. Root: PaymentError. Subclasses: InvalidCard, InsufficientFunds, RateLimitExceeded. Each should have meaningful attributes (e.g. InvalidCard carries the card last4; InsufficientFunds carries account_id, requested, available; RateLimitExceeded carries retry_after_seconds). Write a function charge(card, amount) that raises one of them based on simple inputs. In the calling code, demonstrate catching the broad PaymentError AND specific subclasses, using raise from once.
Progress
Progress is local-only — sign in to sync across devices.