How does Python's exception hierarchy work, and how do you create custom exceptions?
Quick Answer
Every exception ultimately derives from BaseException. Almost all user code should derive from Exception (its subclasses cover things you're expected to catch — BaseException also covers SystemExit/KeyboardInterrupt, which normally shouldn't be caught by a generic handler). Custom exceptions are just classes subclassing Exception (or a more specific built-in exception), typically adding fields for structured error context.
Detailed Answer
The hierarchy, top-down
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ValueError
├── TypeError
├── KeyError, IndexError (-> LookupError)
├── OSError (FileNotFoundError, PermissionError, ...)
├── ArithmeticError (ZeroDivisionError, ...)
└── ... (all "normal" errors your code raises/catches)
Exception is the practical root for everything you raise and catch in
application code. BaseException additionally covers SystemExit (raised
by sys.exit()) and KeyboardInterrupt (Ctrl+C). These deliberately
sit outside Exception so that a broad except Exception: doesn't
accidentally swallow a user's Ctrl+C or a deliberate sys.exit() call.
Defining a custom exception
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
super().__init__(
f"Cannot withdraw {amount}: balance is only {balance}"
)
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
withdraw(100, 150)
except InsufficientFundsError as e:
print(e) # the formatted message
print(e.balance, e.amount) # structured fields for programmatic handling
Subclassing Exception and calling super().__init__(message) gives you
the standard string representation (str(e)) for free, while attaching
extra attributes (self.balance, self.amount) lets calling code inspect
structured error details instead of parsing a message string.
Building an exception hierarchy for a library/app
class AppError(Exception):
'''Base class for all errors raised by this application.'''
class ValidationError(AppError):
pass
class NotFoundError(AppError):
pass
class PermissionDeniedError(AppError):
pass
try:
do_something()
except AppError as e: # catches ANY of the app's custom errors
log.error(e)
except Exception: # anything unexpected/unrelated to the app's domain
raise
Defining one base exception per library/module (AppError) and deriving
all specific errors from it lets calling code choose the granularity it
needs: catch a specific subtype, or catch the whole family via the shared
base, without needing a tuple of every specific exception class.
When to subclass a built-in instead
class InvalidEmailError(ValueError): # "is a" ValueError -- caller code that
pass # already catches ValueError still works
If your custom error is genuinely a more specific case of a built-in
(a malformed email is a kind of ValueError), subclassing the built-in
lets existing code that catches the built-in continue to work correctly
without changes.
Exception is the practical base for
application errors (BaseException also covers SystemExit/
KeyboardInterrupt, deliberately excluded from Exception so broad
handlers don't eat them). Custom exceptions are ordinary subclasses,
usually of Exception or a relevant built-in, adding structured fields
via __init__. A shared base exception per module/library lets
callers choose how broadly to catch.