Skip to content
Python Basics: Preserving Causes with Exception Chaining

Python Basics: Preserving Causes with Exception Chaining

October 18, 2026

A low-level failure may need a clearer explanation at the boundary of your application. Exception chaining lets you raise a meaningful new error while retaining the original cause for debugging. These examples handle expected failures so every script exits successfully.

These three standalone examples were tested on Linux with Python 3.12.14. They require no third-party packages. Assertions check the demonstrated results; run the commands normally, without Python’s -O option.

Prepare a scratch directory

You need Bash, a text editor, Python 3 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8. The setup stops the shell if creating or entering the scratch directory fails.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Translate an input failure without losing its cause

Save this as parse_quantity.py:

class QuantityError(ValueError):
    pass


def parse_quantity(text):
    try:
        return int(text)
    except ValueError as cause:
        raise QuantityError("Quantity must be a whole number") from cause


assert parse_quantity("12") == 12
try:
    parse_quantity("twelve")
except QuantityError as error:
    assert isinstance(error.__cause__, ValueError)
    print(str(error))
    print(f"Cause: {type(error.__cause__).__name__}")
else:
    raise AssertionError("Expected QuantityError")

Run it:

python3 parse_quantity.py

Output:

Quantity must be a whole number
Cause: ValueError

The caller handles QuantityError while a debugger can inspect its cause. The narrow except clause translates a known conversion failure. Catching every Exception here could hide unrelated programming mistakes behind a misleading quantity message.

2. Re-raise the same exception when translation is unnecessary

Save this as reraise_failure.py:

def parse_count(text):
    try:
        return int(text)
    except ValueError:
        print("Count conversion failed")
        raise


try:
    parse_count("many")
except ValueError as error:
    assert error.__cause__ is None
    print(f"Caller received: {type(error).__name__}")
else:
    raise AssertionError("Expected ValueError")

Run it:

python3 reraise_failure.py

Output:

Count conversion failed
Caller received: ValueError

A bare raise inside an exception handler re-raises the active exception. Use it when you want to observe a failure and let the original exception continue to the caller. No new application-specific error is needed in this example.

3. Suppress displayed context deliberately

Save this as hide_internal_context.py:

def require_setting(settings, name):
    try:
        return settings[name]
    except KeyError:
        raise ValueError(f"Required setting is missing: {name}") from None


try:
    require_setting({}, "theme")
except ValueError as error:
    assert error.__cause__ is None
    assert isinstance(error.__context__, KeyError)
    assert error.__suppress_context__ is True
    print(str(error))
    print(f"Context display suppressed: {error.__suppress_context__}")
    print(f"Stored context: {type(error.__context__).__name__}")
else:
    raise AssertionError("Expected ValueError")

Run it:

python3 hide_internal_context.py

Output:

Required setting is missing: theme
Context display suppressed: True
Stored context: KeyError

from None suppresses the original context in the normal traceback display; it does not erase the stored context. This can simplify a public-facing error, but it is not a way to remove secrets from memory or logs. Prefer explicit chaining when the original failure is useful to someone diagnosing the problem.

When to use this

These scripts print stable summaries rather than interpreter-dependent traceback formatting. In a real uncaught chained failure, Python displays the cause and the new exception together. The official exception-chaining tutorial explains raise … from and context suppression.

Clean up

In the same terminal, remove only the three example files, return to your original directory, and remove the empty scratch directory:

rm -- parse_quantity.py reraise_failure.py hide_internal_context.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on