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.
Interview-ready summary: 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__ — and a shared base exception per module/library lets
callers choose how broadly to catch.