How do I define and use a custom exception in Python?
Short answer
Subclass Exception to create a named error your callers can catch specifically. A custom exception makes failures self-describing and lets callers handle your errors without catching everything.
The short version
Subclass Exception to create your own error type. This lets callers catch your specific failure rather than everything, and the name itself documents what went wrong. Define a small hierarchy for a library, and raise the most specific type you can.
Defining one
class DataError(Exception):
"""Raised when input data fails a validation check."""
def load(rows):
if not rows:
raise DataError("no rows to load")
return rows
try:
load([])
except DataError as err:
print(f"could not load: {err}")
That is the whole pattern: a class inheriting from Exception, usually with nothing in the body but a docstring.
Why not just raise ValueError
You can, and for a small script that is fine. A custom type earns its place when a caller needs to distinguish your failure from any other. If your function raises ValueError, a caller catching ValueError also catches value errors from anything else in the block, including bugs they did not anticipate.
try:
load(rows)
except DataError: # only our failure, nothing else
...
This is precision: the caller handles what they meant to handle and lets genuine bugs surface.
A small hierarchy
For a library, define one base error and specific subclasses beneath it.
class PipelineError(Exception):
"""Base class for every error this package raises."""
class MissingColumnError(PipelineError):
...
class BadValueError(PipelineError):
...
Now callers can catch PipelineError to handle anything from your code, or a specific subclass to handle one case. This is why the standard library has a hierarchy rather than one error type.
Carrying useful information
Give the exception attributes when the handler needs details, rather than making it parse the message text.
class MissingColumnError(PipelineError):
def __init__(self, column):
self.column = column
super().__init__(f"missing required column: {column}")
try:
...
except MissingColumnError as err:
print(err.column) # the handler can act on the actual value
Calling super().__init__ keeps the readable message while self.column gives structured access.
Raise early, with a useful message
The message should say what was expected and what was found, because it is often the only thing a future debugger sees.
raise BadValueError(f"amount must be positive, got {amount}")
A message like "invalid input" is nearly useless. Naming the field and the offending value turns a mystery into a two-second fix.
Do not use exceptions for ordinary flow
Exceptions are for exceptional conditions, not for expected outcomes. A lookup that legitimately finds nothing should return None or an empty result, not raise. Reserve exceptions for cases where the code genuinely cannot continue, and the failure needs a caller's attention.