C.W.K.
Stream
Lesson 03 of 06 · published

Custom Exception Classes — Designing Your Own Error Types

~18 min · custom-exception, exception-class, raise

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Why custom exceptions matter

The built-in exceptions are general-purpose: ValueError, TypeError, RuntimeError. They tell you a category of problem but not the specific failure your code experienced. A custom exception class — InsufficientFunds, RateLimitExceeded, InvalidUserToken — communicates intent and lets callers handle exactly the cases they care about.

Inheriting from the right base

Always inherit from Exception, not BaseException. BaseException is the root for things like SystemExit and KeyboardInterrupt that shouldn't be caught by general handlers. Your custom exceptions should slot into the normal "recoverable error" tree under Exception.

Adding context — attributes on the exception

The simplest custom exception is just class MyError(Exception): pass. The next step up: a custom __init__ that captures relevant data — InsufficientFunds(account_id, requested, available). Now exception handlers can inspect e.account_id and respond intelligently.

Exception hierarchies — when you have many related errors

If your library has a family of related errors, build a small hierarchy. class ApiError(Exception): pass as the root, then RateLimitExceeded(ApiError), AuthenticationFailed(ApiError). Callers can catch ApiError for "anything from this library" or specific subclasses for fine-grained handling.

raise from — chaining causes

When one exception leads to another — you caught a low-level FileNotFoundError and want to raise a higher-level ConfigMissing — use raise ConfigMissing(...) from original_exc. Python will print both, with the chain shown. The __cause__ attribute on the new exception points to the original.

Code

Minimal custom exception·python
class InsufficientFunds(Exception):
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFunds(f"asked for {amount}, balance {balance}")
    return balance - amount

try:
    withdraw(100, 200)
except InsufficientFunds as e:
    print("caught:", e)
Custom exception with structured attributes·python
class InsufficientFunds(Exception):
    def __init__(self, account_id, requested, available):
        super().__init__(
            f"account {account_id}: requested {requested}, available {available}"
        )
        self.account_id = account_id
        self.requested = requested
        self.available = available

try:
    raise InsufficientFunds("acc-42", 200, 100)
except InsufficientFunds as e:
    print("message:", e)
    print("shortfall:", e.requested - e.available)
    # handlers can use the structured fields
    if e.account_id.startswith("acc-"):
        print("this is a regular account")
Exception hierarchy — fine-grained catching·python
class ApiError(Exception):
    """Base for all API client errors."""

class RateLimitExceeded(ApiError):
    pass

class AuthenticationFailed(ApiError):
    pass

class ServerError(ApiError):
    pass

def call_api(token):
    if not token:
        raise AuthenticationFailed("no token")
    if token == "limited":
        raise RateLimitExceeded("slow down")
    raise ServerError("500 internal")

# Caller can catch broadly
try:
    call_api(None)
except ApiError as e:
    print("some API problem:", type(e).__name__, e)

# Or narrowly
try:
    call_api("limited")
except RateLimitExceeded:
    print("backing off")
except ApiError as e:
    print("other api error:", e)
raise from — chaining the cause·python
class ConfigError(Exception):
    pass

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise ConfigError(f"config not found at {path}") from e

try:
    load_config("/nonexistent.yaml")
except ConfigError as e:
    print("caught:", e)
    print("caused by:", e.__cause__)

# With `raise X from Y`, traceback prints:
#   FileNotFoundError: ...
#   The above exception was the direct cause of the following exception:
#   ConfigError: ...

External links

Exercise

Build an exception hierarchy for a fictional payment module. Root: PaymentError. Subclasses: InvalidCard, InsufficientFunds, RateLimitExceeded. Each should have meaningful attributes (e.g. InvalidCard carries the card last4; InsufficientFunds carries account_id, requested, available; RateLimitExceeded carries retry_after_seconds). Write a function charge(card, amount) that raises one of them based on simple inputs. In the calling code, demonstrate catching the broad PaymentError AND specific subclasses, using raise from once.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.