Skip to content

Python · Errors & Exceptions

Python Custom Exceptions Explained — The Exception Hierarchy and Designing Your Own Errors

3 min read Updated 2026-06-19 Share:

Practice Custom Exceptions & the Hierarchy interview questions

Python custom exceptions, explained

Python ships with a rich tree of built-in exceptions, and you can extend it with your own. Understanding the hierarchy tells you what to catch and what to subclass; designing good custom exceptions makes your library's failures easy for callers to handle precisely.

The built-in hierarchy

Every exception descends from BaseException. Almost all normal errors descend from Exception — the level you should catch and subclass. A few system-level signals sit beside it, deliberately outside Exception.

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 ├── GeneratorExit
 └── Exception
      ├── ValueError, TypeError, KeyError, IndexError, ...
      ├── ArithmeticError → ZeroDivisionError
      └── OSError → FileNotFoundError, PermissionError, ...

This is why you catch Exception, not BaseException: catching the latter would also swallow KeyboardInterrupt (Ctrl-C) and SystemExit, which you almost never want.

Defining a custom exception

The minimum is a class that inherits from Exception. Often that empty body is enough — the name carries the meaning.

class ValidationError(Exception):
    """Raised when input fails validation."""

raise ValidationError("email is required")

Inheriting from Exception gives you message storage, str(), and args for free. Avoid subclassing BaseException directly — your errors should be catchable by ordinary except Exception handlers.

A base class per package

A widely-used pattern: give your library one base exception, then derive specific ones from it. Callers can catch the base to handle anything from your library, or a specific subclass for fine control.

class PaymentError(Exception):
    """Base for all payment-related errors."""

class CardDeclined(PaymentError):
    pass

class InsufficientFunds(PaymentError):
    pass

# Caller chooses the granularity:
try:
    charge(card)
except InsufficientFunds:
    top_up()
except PaymentError:            # catches any other payment failure
    log_and_alert()

This is the structure used by libraries like requests (RequestException base) — it makes your error surface predictable.

Adding useful attributes

Exceptions are just objects, so attach data callers need to react. Override __init__, but remember to call super().__init__ so the message still works.

class APIError(Exception):
    def __init__(self, message, *, status_code, response=None):
        super().__init__(message)        # sets args/str
        self.status_code = status_code
        self.response = response

try:
    call_api()
except APIError as e:
    if e.status_code == 429:
        backoff_and_retry()

Structured attributes beat forcing callers to parse the message string.

Raising and re-raising well

Raise the most specific type you have, and when translating a lower-level error, chain it so the original cause survives in the traceback.

def load_user(uid):
    try:
        row = db.fetch(uid)
    except db.DBError as e:
        raise UserNotFound(f"no user {uid}") from e   # preserves the cause
    if row is None:
        raise UserNotFound(f"no user {uid}")

raise from keeps debugging information; swallowing the cause throws it away.

When NOT to create a new exception

Don't invent a type when a built-in already says it precisely. Bad arguments → ValueError or TypeError; missing key → KeyError; unsupported operation → NotImplementedError. Custom exceptions earn their keep when callers need to catch your specific failure or when the error is a domain concept the standard library has no name for.

Recap

All exceptions descend from BaseException, but you catch and subclass Exception so you don't swallow KeyboardInterrupt/SystemExit. Define custom errors by subclassing Exception — often just for the name — and give a library one base class with specific subclasses so callers pick their granularity. Attach structured attributes via a super().__init__-calling constructor, chain translated errors with raise ... from, and prefer a precise built-in (ValueError, KeyError) when one already fits.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel