W3docs

Python raise and Custom Exceptions

Learn how to use Python's raise statement, chain exceptions with raise...from, and create custom exception classes for clear, maintainable error handling.

Python lets you do more than catch errors — you can also signal them deliberately with the raise statement and create your own exception types to represent domain-specific problems. This chapter builds on Python Try...Except and covers:

  • The raise statement — raising built-in exceptions
  • Re-raising exceptions inside an except block
  • Exception chaining with raise ... from
  • Creating custom exception classes
  • Building an exception hierarchy for a real application
  • The assert statement and when to use it

The raise Statement

The raise statement lets you throw an exception at any point in your code. The most common form passes an exception instance with a descriptive message:

raise ExceptionType("message")

Use raise when your code detects a problem that the caller must handle. For example, a function that accepts an age should reject negative values immediately rather than silently proceeding:

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

try:
    set_age(-1)
except ValueError as e:
    print(e)
# Output: Age cannot be negative

Choosing the Right Built-in Exception

Python's built-in exception types carry meaning. Picking the right one makes your API easier to understand and allows callers to handle different error categories separately.

ExceptionWhen to raise it
ValueErrorArgument has the right type but an invalid value (age = -1)
TypeErrorArgument has the wrong type (age = "old")
KeyErrorA required dictionary key is missing
IndexErrorA sequence index is out of range
FileNotFoundErrorA required file does not exist
PermissionErrorThe process lacks the rights to perform an operation
RuntimeErrorA general runtime problem that doesn't fit a narrower type
NotImplementedErrorA method exists in a base class but must be overridden

Raising ValueError for a wrong value is much more informative than raising a bare Exception, because callers can write except ValueError to handle exactly that case.

Re-raising an Exception

Sometimes you want to do something on an exception — log it, clean up a resource — and then let the same exception propagate to the caller unchanged. Call raise with no arguments inside an except block to re-raise the current exception:

def read_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        print(f"Warning: config file not found at {path}")
        raise  # re-raise the original FileNotFoundError

try:
    read_config("missing.cfg")
except FileNotFoundError as e:
    print(f"Caught: {e}")
# Output:
# Warning: config file not found at missing.cfg
# Caught: [Errno 2] No such file or directory: 'missing.cfg'

Using bare raise preserves the original traceback, which makes debugging much easier than catching and re-raising e as a new exception.

Exception Chaining with raise ... from

When you catch one exception and raise a different one, Python automatically records the original exception as the context of the new one. You can make this relationship explicit — and meaningful — using raise NewException from original:

def load_data(path):
    try:
        with open(path) as f:
            return f.read()
    except OSError as e:
        raise RuntimeError("Failed to load configuration") from e

try:
    load_data("config.json")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Caused by: {e.__cause__}")
# Output:
# Error: Failed to load configuration
# Caused by: [Errno 2] No such file or directory: 'config.json'

When Python prints the traceback it shows both exceptions in order, making it clear that the RuntimeError was a direct consequence of the OSError. This is especially useful in library code where you want to translate low-level OS errors into higher-level domain errors without hiding the root cause.

Suppressing the Chain with raise ... from None

Occasionally the original exception is an implementation detail you do not want to expose. Pass None as the cause to hide it:

def fetch(url):
    try:
        raise ConnectionError("timeout")
    except ConnectionError:
        raise RuntimeError("Network unavailable") from None

try:
    fetch("http://example.com")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Cause hidden: {e.__cause__}")
# Output:
# Error: Network unavailable
# Cause hidden: None

The traceback will only show the RuntimeError. Use this sparingly — hiding the root cause makes debugging harder for library consumers.

Creating Custom Exception Classes

Built-in exceptions cover common programming errors, but they are too generic for domain problems. If your e-commerce application raises a plain ValueError when a payment fails, callers cannot distinguish that from a bad function argument. Custom exception classes solve this.

A custom exception is simply a class that inherits from Exception (or one of its subclasses):

class InsufficientFundsError(Exception):
    """Raised when a bank account has insufficient funds."""
    def __init__(self, amount, balance):
        self.amount = amount
        self.balance = balance
        super().__init__(
            f"Cannot withdraw {amount}: balance is only {balance}"
        )

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(amount, self.balance)
        self.balance -= amount
        return self.balance

account = BankAccount(100)
try:
    account.withdraw(150)
except InsufficientFundsError as e:
    print(e)
    print(f"You tried to withdraw: {e.amount}")
    print(f"Available balance:     {e.balance}")
# Output:
# Cannot withdraw 150: balance is only 100
# You tried to withdraw: 150
# Available balance:     100

Key points about this pattern:

  • super().__init__(message) sets the human-readable string returned by str(e).
  • Extra attributes (self.amount, self.balance) let callers access structured data from the exception, not just a string.
  • A clear docstring documents when the exception should be raised.

Building an Exception Hierarchy

Real applications often have many related error types. Grouping them under a shared base class lets callers catch either the specific error or the whole category:

class AppError(Exception):
    """Base class for all application errors."""

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

class DatabaseError(AppError):
    """Raised when a database operation fails."""

def validate_username(name):
    if len(name) < 3:
        raise ValidationError(f"Username '{name}' is too short (min 3 chars)")

try:
    validate_username("ab")
except ValidationError as e:
    print(f"Validation failed: {e}")
except AppError as e:
    print(f"Application error: {e}")
# Output:
# Validation failed: Username 'ab' is too short (min 3 chars)

A caller that only wants to catch database errors can write except DatabaseError. A caller that wants to catch any problem from your library can write except AppError. This mirrors the design of Python's own exception hierarchy, where OSError groups FileNotFoundError, PermissionError, and several others.

Guidelines for Custom Exceptions

  • Inherit from Exception, not BaseException. BaseException is the root of Python's hierarchy and also includes SystemExit and KeyboardInterrupt, which should not be caught accidentally.
  • End the class name in Error for exceptions that signal a problem. This matches Python's own naming (ValueError, TypeError, IOError).
  • Keep the class minimal unless you need extra attributes. An empty body with a docstring is perfectly valid.
  • Put exceptions in a dedicated module (e.g., exceptions.py) for larger projects so callers can import them without pulling in the rest of your code.

The assert Statement

assert is a lightweight way to express invariants — conditions that must be true for your code to be correct:

def divide(a, b):
    assert b != 0, "Divisor must not be zero"
    return a / b

try:
    divide(10, 0)
except AssertionError as e:
    print(f"AssertionError: {e}")

print(divide(10, 2))
# Output:
# AssertionError: Divisor must not be zero
# 5.0

assert condition, message raises AssertionError with the given message when condition is False.

Important limitation: Python removes assert statements when run with the -O (optimize) flag. This means:

  • Use assert for internal consistency checks and debugging aids only.
  • Use raise with a proper exception for user-facing input validation and public API checks that must always run.

Common Mistakes

Catching and silently ignoring exceptions

# Bad — the error disappears
try:
    result = risky_operation()
except Exception:
    pass

# Better — at minimum, log or re-raise
try:
    result = risky_operation()
except Exception as e:
    print(f"Operation failed: {e}")
    raise

Raising a string instead of an exception

# Wrong — strings are not exceptions
raise "something went wrong"  # TypeError

# Correct
raise ValueError("something went wrong")

Catching BaseException accidentally

# Dangerous — this catches KeyboardInterrupt and SystemExit too
except BaseException:
    ...

# Use Exception instead
except Exception:
    ...

Summary

TechniqueWhen to use it
raise ExceptionType("msg")Signal a problem the caller must handle
raise (bare)Re-raise the current exception after logging or cleanup
raise NewError(...) from originalTranslate a low-level error into a higher-level one, preserving the cause
raise NewError(...) from NoneTranslate an error while hiding the internal cause
Custom exception classGive domain-specific errors a unique, catchable type
Exception hierarchyAllow callers to catch narrow or broad categories of errors
assertVerify internal invariants during development only

For the full picture of catching and handling exceptions, see Python Try...Except. To understand how custom exceptions fit into class design, review Python Classes and Objects and Python Inheritance.

Practice

Practice
Which statement correctly raises a ValueError with the message 'invalid input'?
Which statement correctly raises a ValueError with the message 'invalid input'?
Practice
What does bare raise (with no argument) do inside an except block?
What does bare raise (with no argument) do inside an except block?
Practice
Which base class should a custom exception inherit from?
Which base class should a custom exception inherit from?
Was this page helpful?