Skip to main content Brad's PyNotes

PEP 3134: Exception Chaining and Embedded Tracebacks

TL;DR

PEP 3134 introduced exception chaining in Python 3, allowing exceptions to preserve their context when new exceptions occur during error handling. The raise ... from syntax creates explicit chains, while Python automatically captures implicit chains, both appearing in tracebacks to simplify debugging.

Interesting!

Python 3 displays both exceptions in the traceback with descriptive messages explaining their relationship—“During handling of the above exception, another exception occurred” for implicit chains and “The above exception was the direct cause of the following exception” for explicit raise ... from chains.

The Problem Python 2 Had

Before Python 3, when an exception occurred while handling another exception, the original exception vanished:

python code snippet start

# Python 2 behavior
try:
    open('nonexistent.txt')
except IOError:
    print(1/0)  # Original IOError is lost!

python code snippet end

Developers lost valuable debugging information about what triggered the error chain. Additionally, exception information was scattered across three separate sys.exc_info() values, making exception handling verbose and error-prone.

Three New Exception Attributes

PEP 3134 introduced three standard attributes on exception objects:

python code snippet start

exception.__context__   # Implicit chaining (automatic)
exception.__cause__     # Explicit chaining (via raise from)
exception.__traceback__ # Direct traceback storage

python code snippet end

These attributes preserve the complete error history while simplifying exception introspection.

Implicit Exception Chaining

Python automatically captures context when exceptions occur during error handling:

python code snippet start

try:
    result = 1 / 0
except ZeroDivisionError:
    # Oops, another error during cleanup
    int('invalid')  # ValueError is chained to ZeroDivisionError

python code snippet end

The traceback shows both exceptions:

code snippet start

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    result = 1 / 0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    int('invalid')
ValueError: invalid literal for int() with base 10: 'invalid'

code snippet end

Explicit Exception Chaining with raise from

Use raise ... from when intentionally wrapping or translating exceptions:

python code snippet start

class DatabaseError(Exception):
    pass

def connect_to_database(filename):
    try:
        return open(filename)
    except IOError as exc:
        raise DatabaseError('Failed to open database') from exc

python code snippet end

This pattern is crucial for libraries that need to provide domain-specific exceptions while preserving underlying error details. The __cause__ attribute stores the original exception.

Suppressing Context with raise from None

Sometimes the original exception is irrelevant to API consumers:

python code snippet start

def validate_user_input(data):
    try:
        return int(data)
    except ValueError:
        raise InputError('Invalid number format') from None

python code snippet end

The from None syntax sets __cause__ to None and suppresses __context__ display, showing only the new exception.

When to Use Exception Chaining

Use implicit chaining (automatic): Let Python handle secondary exceptions that occur unexpectedly during error recovery or cleanup.

Use explicit chaining (raise ... from exc): When converting exceptions to domain-specific types, wrapping library exceptions, or providing more meaningful errors while preserving debug information.

Use from None: When the original exception is an implementation detail that adds no value to API consumers.

Practical Pattern: Exception Translation

This pattern appears frequently in well-designed libraries:

python code snippet start

class APIError(Exception):
    """High-level application error"""
    pass

def fetch_data(url):
    try:
        response = urllib.request.urlopen(url)
        return json.loads(response.read())
    except urllib.error.URLError as exc:
        raise APIError(f'Failed to fetch {url}') from exc
    except json.JSONDecodeError as exc:
        raise APIError('Invalid JSON response') from exc

python code snippet end

Users see meaningful APIError exceptions but can inspect __cause__ for debugging low-level issues.

Exception chaining makes Python’s error handling more transparent, preserving the full story of what went wrong while giving developers control over how exceptions are presented.

Error and Exception Handling Tutorial covers the broader topic of Python exception handling, while PEP 343: The with Statement explores context managers for clean exception handling.

Reference: PEP 3134 – Exception Chaining and Embedded Tracebacks