Q&A library Python

How do I define and use a custom exception in Python?

Python Intermediate classeserrorsexceptions

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.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project