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 storagepython 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 ZeroDivisionErrorpython 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 excpython 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 Nonepython 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 excpython 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