Custom Exception Hierarchies
Subclass Exception to architect domain-specific error trees and communicate actionable failure details.
One Base Class, Many Specific Failures
A well-designed subsystem defines a single base exception (e.g. CommerceError(Exception)) and then specific subclasses for each distinct failure mode (e.g. InsufficientStockError(CommerceError)). Callers can catch broadly with one except CommerceError: or narrowly with the specific subclass — the hierarchy gives them the choice.
Custom exceptions always inherit from Exception, never directly from BaseException — that base is reserved for control-signal exceptions like SystemExit and KeyboardInterrupt that a normal except Exception: should never accidentally intercept.
# caller can't distinguish failure kinds
class InsufficientStockError(CommerceError): ...
A custom exception hierarchy is like a hospital's triage categories: “Emergency” is the broad label a receptionist can route on, but “Cardiac Emergency” and “Trauma Emergency” are specific enough for the right specialist to respond precisely.
A Two-Level Domain Hierarchy
Run the snippet below to see a specific subclass caught two different ways:
class CommerceError(Exception):
"""Root exception for all commerce subsystem failures."""
pass
class InventoryError(CommerceError):
"""Raised when inventory operations fail."""
pass
class InsufficientStockError(InventoryError):
"""Raised when an item does not have enough quantity."""
pass
class PaymentError(CommerceError):
"""Raised when payment processing fails."""
pass
def checkout(sku, quantity_requested, stock_on_hand):
if quantity_requested > stock_on_hand:
raise InsufficientStockError(f"SKU {sku} needs {quantity_requested}, but only {stock_on_hand} available")
print("Stock reserved successfully")
try:
checkout("BOOK-404", 5, 2)
except InsufficientStockError as err:
print(f"Specific handler: {err}")
except CommerceError as err:
print(f"Broad commerce handler: {err}")One shared base exception per subsystem, always subclassing Exception, never BaseException.
Store fields like status_code or error_code on the instance so callers inspect them programmatically instead of parsing a message string.
Sets __cause__, preserving the original low-level error in the traceback for diagnostics.
Suppresses the implicit chain at an API boundary so internal implementation details never leak to external callers.
Explore a Real Domain Exception Tree
Click any node to see its parent class, purpose, and structured fields:
The subsystem's single base exception. Callers can catch just this to handle any commerce failure.
messageisinstance() vs issubclass() vs Chaining
issubclass() inspects type relationships between two classes; isinstance() checks whether a concrete caught instance belongs to a category — both drive dynamic retry routing.
isinstance() — check a live object
isinstance(err, TransientError)True only if this specific caught instance is (or subclasses) TransientError.
isinstance(err, (ValueError, TypeError))Accepts a tuple — true if err matches any listed type.
issubclass() — check a type relationship
issubclass(TimeoutError, TransientError)True at the class level — no instance needed, just the type objects.
issubclass(TransientError, NetworkError)Confirms the hierarchy shape itself, useful for validating a class design.
Exception chaining
raise APIError(...) from errPreserves __cause__ so the original low-level error stays visible in the traceback.
raise APIError(...) from NoneSuppresses the implicit context — hides internal details from external API callers.
Worked Hands-on Walkthroughs
Progress step-by-step from a basic domain tree through structured API errors to dynamic retry classification:
Defining a Hierarchical Domain Exception Tree
Because InsufficientStockError inherits from CommerceError, a caller can either handle specific out-of-stock conditions or catch all commerce errors uniformly.
class CommerceError(Exception):
"""Root exception for all commerce subsystem failures."""
pass
class InventoryError(CommerceError):
"""Raised when inventory operations fail."""
pass
class InsufficientStockError(InventoryError):
"""Raised when an item does not have enough quantity."""
pass
class PaymentError(CommerceError):
"""Raised when payment processing fails."""
pass
def checkout(sku, quantity_requested, stock_on_hand):
if quantity_requested > stock_on_hand:
raise InsufficientStockError(f"SKU {sku} needs {quantity_requested}, but only {stock_on_hand} available")
print("Stock reserved successfully")
try:
checkout("BOOK-404", 5, 2)
except InsufficientStockError as err:
print(f"Specific handler: {err}")
except CommerceError as err:
print(f"Broad commerce handler: {err}")Because InsufficientStockError inherits from CommerceError, a caller can either handle specific out-of-stock conditions or catch all commerce errors uniformly.
Creating Exceptions with Machine-Readable Fields
Structured attributes allow API frameworks to directly map exception instances to JSON error responses and status codes.
class APIError(Exception):
def __init__(self, message, status_code=400, error_code="BAD_REQUEST", details=None):
super().__init__(message)
self.status_code = status_code
self.error_code = error_code
self.details = details or {}
def to_dict(self):
return {
"error": self.error_code,
"message": str(self),
"status": self.status_code,
"details": self.details
}
def validate_user_payload(data):
if "email" not in data:
raise APIError(
"Validation failed for payload",
status_code=422,
error_code="VALIDATION_FAILED",
details={"missing_fields": ["email"]}
)
try:
validate_user_payload({"username": "ada"})
except APIError as err:
print("HTTP Status:", err.status_code)
print("JSON Response:", err.to_dict())Structured attributes allow API frameworks to directly map exception instances to JSON error responses and status codes.
Dynamic Error Classification and Retry Logic
issubclass() inspects type relationships, whereas isinstance() checks whether a concrete runtime error object belongs to an exception category.
class NetworkError(Exception): pass
class TransientError(NetworkError): pass
class PermanentError(NetworkError): pass
class TimeoutError(TransientError): pass
class RateLimitExceededError(TransientError): pass
class BadCredentialsError(PermanentError): pass
# 1. Class hierarchy introspection with issubclass
print("Timeout is transient:", issubclass(TimeoutError, TransientError))
print("Timeout is network error:", issubclass(TimeoutError, NetworkError))
print("Transient is timeout:", issubclass(TransientError, TimeoutError))
def handle_dispatch(err):
# 2. Instance classification with isinstance
if isinstance(err, TransientError):
return "SCHEDULE_RETRY"
elif isinstance(err, PermanentError):
return "ALERT_OPERATOR"
return "ABORT"
print("Dispatch RateLimit:", handle_dispatch(RateLimitExceededError("Backoff 5s")))
print("Dispatch BadCredentials:", handle_dispatch(BadCredentialsError("Invalid secret")))issubclass() inspects type relationships, whereas isinstance() checks whether a concrete runtime error object belongs to an exception category.
Common Traps & How to Avoid Them
Study the mistakes and clean fixes below before designing production exception hierarchies:
# Mistyped variable or missing closing parenthesis
result = custom_exception_hierarchies_val# Declare and verify variables before execution
val = 10
print("Defined correctly:", val)Beginners frequently encounter typos or forgotten definitions before using an identifier.
# Modifying shared state or unexpected variable shadowing
def update_state(data=[]):
data.append(1)
return datadef update_state(data=None):
if data is None:
data = []
data.append(1)
return dataDefault arguments in Python are evaluated once at function definition time, not at invocation time.
# Opening resources in a tight loop without closing or bounding memory
files = [open(f"log_{i}.txt", "w") for i in range(5000)]for i in range(5):
with open(f"log_{i}.txt", "w") as f:
f.write("Safe")Failing to release system resources leads to process termination under heavy production workloads.
Real-World Systems in Action
See how scripts, microservices, and payment gateways rely on rich domain hierarchies for resilient error routing:
Local Developer Automation Tool
Individual developers and IT administrators automate everyday repetitive workflows.
A clean standalone script providing fast, local automation with zero dependencies.
class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError("Withdrawal exceeds balance!")
return balance - amountBackend API Service Integration
SaaS platforms run backend microservices processing requests concurrently.
A modular service handler integrating validation and structured responses.
# Service integration layer
def handle_request(payload: dict) -> dict:
return {"status": "success", "processed": True}High-Throughput Cloud Architecture
Payment gateways (Stripe, Adyen) provide rich domain exception hierarchies (CardExpiredError, FraudBlockedError) for API consumers.
Payment gateways (Stripe, Adyen) provide rich domain exception hierarchies (CardExpiredError, FraudBlockedError) for API consumers.
# High-resilience production pattern
import time
def process_stream(events: list) -> int:
processed = 0
for event in events:
if event:
processed += 1
return processedKey Points & Concept Review
- Always inherit from Exception, never directly from BaseException, to preserve control-signal handling.
- Establish a single subsystem base exception so callers can catch all domain-specific errors with a single except clause.
- Enrich exceptions with structured metadata (error codes, payloads, status codes) via __init__ for programmatic inspection.
- Use 'raise ... from err' to preserve the underlying __cause__ and maintain an unbroken diagnostic audit trail.
- Use 'raise ... from None' to prevent internal low-level implementation details from leaking to API callers.
- Use issubclass() to verify inheritance trees between classes and isinstance() to inspect runtime error instances against category tuples.
Frequently Asked Questions & Reference Hub
No production codebase designs its perfect exception tree on the first try. Start with one base class and one or two subclasses; add more only when a caller actually needs to distinguish a new failure mode.
See how libraries design a single subsystem base exception.
Understand when to preserve or suppress the exception context.
Compare type-level checks against instance-level checks.
Learn how to attach machine-readable metadata to an exception.
See real production domain exception trees from payment APIs.
Learn exactly what raise ... from sets on the exception object.
Check whether a caught object belongs to a given exception category.
Verify a class-level inheritance relationship without an instance.
You've Mastered Custom Exception Hierarchies!
You can design a subsystem base exception, attach structured metadata, and classify errors dynamically. Now lock in your knowledge with hands-on practice.
Design a small exception hierarchy with a shared base class.
Earn experience points toward your developer rank as soon as you pass.
Bite-sized knowledge check with instant explanations for every question.
Worked example
Define and catch a domain-specific business error.
class EmptyCartError(Exception): pass
try:
raise EmptyCartError("Cart is empty")
except EmptyCartError as e:
print(f"Caught: {e}")
How it works, step by step
- Custom exceptions subclass Exception.
Custom Exception Hierarchies practice
Define a custom exception class AgeRestrictionError(Exception): pass. Write a function verify_age(age) that raises AgeRestrictionError("Must be at least 18") if age < 18. Test with try/except calling verify_age(16) and print the exception message: Caught: Must be at least 18.
Coding Practice is Locked
Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.
Practice quiz
Check yourself
15 questions with explanations after submission, so you can correct course as you go.
Custom Exception Hierarchies quiz
LockedYou must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.
Lesson incomplete
Not completedCreate an ID or sign in to save lesson completion across devices.