Custom Exceptions
Built-in exceptions can't name every failure your program cares about — custom exception classes let you define errors that speak your application's own vocabulary.
Python lets you define your own exception types by subclassing Exception (or a more specific built-in). A custom exception hierarchy turns generic failures into domain-specific, catchable, self-documenting signals — InsufficientFundsError communicates far more than a bare ValueError, and callers can catch exactly the failures they know how to handle.
Why Custom Exceptions
Built-in exceptions (ValueError, TypeError, KeyError, ...) describe generic Python-level problems. As an application grows, you often want to signal domain-specific failures — 'insufficient funds', 'invalid order state', 'model not trained' — that callers can catch specifically, attach extra data to, and distinguish from unrelated ValueErrors elsewhere in the codebase.
A custom exception is just a class, typically with little or no extra code, that inherits from Exception (directly or indirectly). Its main value is its *name* and *position in a hierarchy* — both are documentation and both are catchable.
Defining a Basic Custom Exception
The minimal custom exception is a one-line class that subclasses Exception and does nothing else — it inherits __init__, args, and __str__ from Exception, so MyError("message") works immediately.
InsufficientFundsError needs no body beyond a docstring; it inherits everything it needs from Exception and is immediately distinguishable from other ValueErrors or generic exceptions in a traceback and in except clauses.
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the available balance."""
def withdraw(balance: float, amount: float) -> float:
if amount > balance:
raise InsufficientFundsError(
f"cannot withdraw {amount}, balance is {balance}"
)
return balance - amountBuilding an Exception Hierarchy
Real applications typically define one base exception for the whole package or module, then subclass it for specific error kinds. This lets callers choose their granularity: catch the specific subclass, or catch the shared base to handle 'any error from this module' generically.
Because except clauses match subclasses, catching BankError as a fallback handles any current or future banking error without needing to enumerate them all — while still allowing precise handling of specific ones first.
Exception hierarchy for a banking module built-in root of ordinary exceptions
class BankError(Exception):
"""Base class for all banking-domain errors."""
class InsufficientFundsError(BankError):
pass
class AccountFrozenError(BankError):
pass
class InvalidAccountError(BankError):
pass
try:
withdraw(account, 500)
except InsufficientFundsError:
offer_overdraft()
except BankError:
log_and_alert() # catches AccountFrozenError, InvalidAccountError, etc.Choosing What to Inherit From
Most custom exceptions should ultimately derive from Exception, not BaseException directly — BaseException is meant for things that should typically NOT be caught by a bare except Exception: (SystemExit, KeyboardInterrupt). Sometimes it makes sense to inherit from a specific built-in (e.g. a custom ConfigKeyError(KeyError)) so existing code that catches the built-in still works, while new code can catch the more specific subclass.
Inheriting from a built-in keeps backward compatibility with generic handlers while letting new code target the specific subclass.
class ConfigKeyError(KeyError):
"""A required configuration key was missing."""
try:
raise ConfigKeyError("database_url")
except KeyError:
print("caught as a KeyError too") # still works, KeyError is a base classAdding Custom Attributes
Override __init__ to attach structured data to the exception, beyond a plain message string. Always call super().__init__(...) so args and str() still behave sensibly for anything that doesn't know about your custom attributes.
Callers that only do str(exc) or log the exception still get a readable message; callers that want structured data can reach exc.balance, exc.amount, exc.shortfall directly.
class InsufficientFundsError(BankError):
def __init__(self, balance: float, amount: float) -> None:
message = f"cannot withdraw {amount}, balance is {balance}"
super().__init__(message)
self.balance = balance
self.amount = amount
self.shortfall = amount - balance
try:
raise InsufficientFundsError(balance=50.0, amount=75.0)
except InsufficientFundsError as exc:
print(exc.shortfall) # 25.0
print(exc) # cannot withdraw 75.0, balance is 50.0Overriding __str__ and __repr__
By default, str(exc) on an Exception subclass shows its args joined (or the single arg if there's one). If you store structured attributes instead of passing a message to super().__init__, override __str__ to keep error messages readable.
Without the __str__ override, printing the exception would just show the tuple ('email', 'missing @ symbol') rendered via the default Exception.__str__ — technically correct but less readable.
class ValidationError(Exception):
def __init__(self, field: str, reason: str) -> None:
self.field = field
self.reason = reason
super().__init__(field, reason)
def __str__(self) -> str:
return f"validation failed for {self.field!r}: {self.reason}"
raise ValidationError("email", "missing @ symbol")
# ValidationError: validation failed for 'email': missing @ symbolDomain-Specific Errors in Practice
Custom exceptions shine at module or package boundaries: a library defines its own exception hierarchy so that users of the library can catch 'anything this library might raise' without needing to know every internal detail, and so library internals (like a stdlib KeyError leaking from an internal dict lookup) don't leak as implementation details into the public API.
A user of this library can except ParserError: to catch every failure mode the library defines, present or future, without enumerating each subclass.
class ParserError(Exception):
"""Base for all errors raised by this parsing library."""
class UnexpectedTokenError(ParserError):
def __init__(self, token: str, position: int) -> None:
super().__init__(f"unexpected token {token!r} at position {position}")
self.token = token
self.position = position
def parse(tokens: list[str]) -> None:
for i, tok in enumerate(tokens):
if tok not in KNOWN_TOKENS:
raise UnexpectedTokenError(tok, i)Common Mistakes and Pitfalls
The most frequent slip is overriding __init__ without calling super().__init__(...), which leaves args empty and can break code (including some logging and traceback formatting) that expects it to be populated.
Always call super().__init__() with whatever should populate args, even if you also store attributes separately.
class BadError(Exception):
def __init__(self, msg):
self.msg = msg # forgot super().__init__(msg)
e = BadError("oops")
print(e.args) # () -- empty, not ('oops',)
print(e) # '' -- empty string, not 'oops'Key Takeaways
Custom exceptions subclass Exception (or a specific built-in) and give domain-specific failures their own name and identity.
Define one shared base exception per module/package, then subclass it for specific error kinds — callers can catch broad or narrow as needed.
Override __init__ to attach structured data, but always call super().__init__(...) so args and default str() still work.
Override __str__ when you store structured attributes instead of a plain message, to keep printed errors readable.
Inherit from a specific built-in exception (not just Exception) when you want existing generic handlers to keep working.
class MyError(Exception): def __init__(self, code): self.code = code class MyError(Exception): def __init__(self, code): super().__init__(f"error code {code}") self.code = code
Skipping super().__init__() leaves args empty and str(exc) returns an empty string, breaking default logging/printing of the exception.
class Error1(Exception): pass class Error2(Exception): pass class Error3(Exception): pass # no shared base class AppError(Exception): pass class Error1(AppError): pass class Error2(AppError): pass class Error3(AppError): pass
Without a common base class, callers who want to catch 'any error from this app' must list every subclass individually instead of catching one shared base.
class ConfigError(BaseException): pass class ConfigError(Exception): pass
Subclassing BaseException directly means a bare except Exception: will NOT catch it, which is almost never what you want for an ordinary application error.
Why define a custom exception instead of just raising ValueError everywhere?
A custom exception gives a failure a specific, catchable name and can carry domain-specific attributes; callers can distinguish it from unrelated ValueErrors and catch it precisely, and it self-documents intent in the code.
What is the benefit of defining a shared base exception class for a module or library?
It lets callers catch 'any error this module raises' with one except clause on the base class, while still allowing precise handling of specific subclasses when needed.
What goes wrong if you override __init__ in a custom exception without calling super().__init__()?
The exception's args tuple stays empty and the default str(exc)/repr(exc) no longer show the message, which can break logging, tracebacks that rely on args, and generic error-printing code.
Should most custom exceptions inherit from Exception or BaseException? Why?
Exception, because BaseException is reserved for things like SystemExit and KeyboardInterrupt that a bare except Exception: deliberately does not catch; inheriting from BaseException would make an ordinary error invisible to normal exception handling.
When would you inherit a custom exception from a specific built-in like KeyError instead of Exception?
When you want backward compatibility: existing code that catches the built-in (KeyError) continues to work unchanged, while new code can catch your more specific subclass.
How do you attach structured data (not just a message) to a custom exception?
Override __init__ to accept the data, call super().__init__(message) to keep args/str() working, and store the extra values as instance attributes (e.g. self.balance = balance) that callers can access via the caught exception object.
Define a small exception hierarchy for a library: a base LibraryError, and two subclasses NotFoundError and PermissionDeniedError.
class LibraryError(Exception): """Base class for all errors from this library.""" class NotFoundError(LibraryError): pass class PermissionDeniedError(LibraryError): pass
LibraryError is the shared base; both specific errors subclass it, so callers can catch either specifically or LibraryError generically.
What does this print, and why?
It prints () and `` (empty string). Because __init__ never calls super().__init__(code), the base Exception's args tuple is never populated, so both args and the default str() are empty.
Write a custom exception OutOfStockError that stores item and requested quantity as attributes, with a readable message via __str__.
class OutOfStockError(Exception): def __init__(self, item: str, requested: int, available: int) -> None: self.item = item self.requested = requested self.available = available super().__init__(item, requested, available) def __str__(self) -> str: return ( f"cannot fulfill {self.requested} of {self.item!r}, " f"only {self.available} available" )
Stores structured attributes for programmatic access and overrides __str__ so printing or logging the exception gives a clear human-readable message.
class MyError(Exception):
def __init__(self, code):
self.code = code
e = MyError(404)
print(e.args)
print(str(e))