Python ships with plenty of built-in exceptions — ValueError, TypeError, KeyError, and more — but they can't describe every failure specific to your own application. When "insufficient balance" or "invalid user age" happens, raising a generic ValueError doesn't tell the calling code (or the next developer reading it) what actually went wrong in domain terms. That's what custom exceptions are for.

Defining a Custom Exception

A custom exception is just a class that inherits from Exception (or one of its built-in subclasses). Most of the time it needs no extra code at all — inheriting is enough to make it a fully working exception type.

class InsufficientBalanceError(Exception):
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientBalanceError(
            f"Cannot withdraw {amount}, balance is only {balance}"
        )
    return balance - amount

try:
    withdraw(100, 250)
except InsufficientBalanceError as e:
    print(f"Transaction failed: {e}")

Output:

Transaction failed: Cannot withdraw 250, balance is only 100

Adding Extra Data to a Custom Exception

You can override __init__ to attach extra information to the exception, which calling code can then inspect — not just a message string.

class InsufficientBalanceError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Cannot withdraw {amount}, balance is only {balance}")

try:
    raise InsufficientBalanceError(balance=100, amount=250)
except InsufficientBalanceError as e:
    print(e)
    print(f"Short by: {e.amount - e.balance}")

Output:

Cannot withdraw 250, balance is only 100
Short by: 150

When a Custom Exception Is Worth Creating

Create a custom exception when the failure represents a meaningful, named concept in your application's domain — something calling code might reasonably want to catch specifically and handle differently from other errors. If you're just going to catch it and print the same generic message as a built-in ValueError would give you, a custom class is probably unnecessary overhead. A good rule of thumb: if you find yourself checking an error message string to figure out what went wrong, that's a sign you should have raised a distinct exception type instead.

Custom exceptions are commonly organized into a small hierarchy — e.g. a base AppError that more specific exceptions inherit from — which pairs naturally with what you learned about inheritance and try/except exception handling.