PythonRaising Exceptions

Raising Exceptions

Catching exceptions is only half of Python’s error-handling story. The other half is deciding when your own code should signal that something is wrong, using the raise keyword. Raising the right exception, with a clear message, at the point where invalid data or an invalid state is first detected, is one of the most effective things you can do for the people (including future you) who will debug the program later.

The `raise` Keyword

raise takes an exception instance (or, less commonly, a bare exception class, which Python instantiates with no arguments) and immediately stops normal execution, unwinding the stack until something catches it.

Python
def set_age(age):
    if age < 0:
        raise ValueError(f"age cannot be negative, got {age}")
    return age

set_age(-5)
Traceback (most recent call last):
  File "example.py", line 6, in <module>
    set_age(-5)
  File "example.py", line 3, in set_age
    raise ValueError(f"age cannot be negative, got {age}")
ValueError: age cannot be negative, got -5
Tip
Prefer Python’s built-in exception types over inventing your own for common cases. `ValueError` for a bad value, `TypeError` for a wrong type, and `KeyError` / `IndexError` for missing lookups are all well understood by anyone reading your code, and by libraries that might catch them.
Writing a Useful Message

The string passed to an exception’s constructor becomes its message, and it is the very first thing a reader sees in a traceback. A message like "invalid input" forces the reader to go digging; a message that includes the offending value and what was expected saves that trip entirely.

Python
def get_discount(code, catalog):
    if code not in catalog:
        raise KeyError(f"unknown discount code {code!r}; valid codes: {sorted(catalog)}")
    return catalog[code]
Re-raising a Caught Exception

Sometimes you want to inspect or log an exception without actually handling it — the caller still needs to see it. A bare raise with no arguments, used inside an except block, re-raises the exception that is currently being handled, preserving its original traceback.

Python
def load_settings(path):
    try:
        return parse(open(path).read())
    except ValueError:
        log.error("settings file at %s is malformed", path)
        raise   # re-raise the same ValueError, traceback intact
Note
Do not write `raise e` when you mean to re-raise — while it looks similar, it resets part of the traceback to point at the `raise e` line instead of preserving the original one. A bare `raise` is the correct way to pass an exception through untouched.
Exception Chaining with `raise ... from ...`

When you catch one exception and raise a different, more meaningful one in response, raise NewException(...) from original links the two together. Python prints both in the traceback, so nothing about the root cause is lost even though the caller only needs to handle the new, higher-level exception type.

Python
class ConfigError(Exception):
    pass

def load_config(path):
    try:
        return parse(open(path).read())
    except FileNotFoundError as e:
        raise ConfigError(f"configuration file missing: {path}") from e
Traceback (most recent call last):
  File "app.py", line 6, in load_config
    return parse(open(path).read())
FileNotFoundError: [Errno 2] No such file or directory: 'settings.ini'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "app.py", line 9, in <module>
    load_config("settings.ini")
  File "app.py", line 8, in load_config
    raise ConfigError(f"configuration file missing: {path}") from e
ConfigError: configuration file missing: settings.ini
Tip
Use `raise NewError(...) from None` when you deliberately want to *suppress* the original exception’s traceback because it would only confuse the caller (for example when translating a low-level parsing error into a clean, user-facing validation error).
`assert` for Internal Invariants

assert checks a condition and raises AssertionError if it is false, optionally with a message. It exists to check things that should be impossible if your own code is correct — internal invariants and programmer assumptions — not to validate data coming from outside the program.

Python
def average(numbers):
    assert len(numbers) > 0, "average() called with an empty list"
    return sum(numbers) / len(numbers)
Warning
Never use `assert` for input validation, permission checks, or anything security-related. When Python is run with the `-O` (optimize) flag, every `assert` statement in the program is stripped out entirely and never executes — so any validation you relied on silently disappears. Use `raise ValueError(...)` (or a custom exception) for anything that must always be checked, regardless of how the interpreter is invoked.
  • raise SomeException("message") — signal a problem with a clear, specific message.

  • raise (no argument) inside except — re-raise the exception currently being handled, unchanged.

  • raise NewException(...) from original — chain a new exception to the one that caused it.

  • assert condition, "message" — check internal invariants only; stripped under -O, never for real validation.