PEP 654: Exception Groups and except* in Python 3.11
Python 3.11 introduced PEP 654, a major enhancement to error handling
that allows programs to raise, catch, and inspect multiple unrelated
exceptions simultaneously. This article explains the mechanics behind
ExceptionGroup and BaseExceptionGroup, details
how the new except* syntax allows selective handling of
partial exception trees, and demonstrates how these additions simplify
error handling in concurrent and asynchronous programming.
The Problem Addressed by PEP 654
Historically, Python could only propagate a single active exception
up the call stack at any given time. This model broke down in concurrent
contexts—such as asyncio task groups, multi-threaded
operations, or parallel worker pools—where multiple operations run
concurrently and several can fail independently at the same time.
Previously, developers had to resort to suppressing all but one
exception, nesting exceptions artificially via __context__
or __cause__, or using custom collection types that did not
integrate with native try...except blocks.
Exception Groups: Grouping Multiple Failures
PEP 654 introduced two new built-in exception types:
ExceptionGroup and BaseExceptionGroup. These
types wrap a list of nested exception instances together with an
explanatory message:
eg = ExceptionGroup(
"Multiple tasks failed",
[
ValueError("Invalid configuration value"),
TypeError("Expected string, got int"),
KeyError("Missing required key"),
],
)Exception groups can be nested, forming a tree structure where
internal nodes are ExceptionGroup instances and leaves are
traditional exceptions (or further groups). Because
ExceptionGroup subclasses Exception, it only
contains standard exceptions. BaseExceptionGroup inherits
directly from BaseException and can encapsulate critical
system-level interrupts such as KeyboardInterrupt and
SystemExit.
Handling Partial Groups
with except*
To work with composite exceptions, Python 3.11 introduced the
except* (except-star) syntax. A standard
except block can only match the entire
ExceptionGroup as a whole. In contrast,
except* matches and extracts specific exception types from
within the group, allowing distinct errors to be addressed
separately.
try:
raise ExceptionGroup(
"Operation failed",
[ValueError("Invalid data"), TypeError("Wrong type"), ValueError("Out of range")]
)
except* ValueError as eg:
# Handles both ValueError instances wrapped in an ExceptionGroup
for exc in eg.exceptions:
print(f"Handled ValueError: {exc}")
except* TypeError as eg:
# Handles the TypeError wrapped in an ExceptionGroup
for exc in eg.exceptions:
print(f"Handled TypeError: {exc}")Key Execution Rules for
except*
- Multiple Matches Execute: Unlike traditional
try...except, where only the first matching branch executes, multipleexcept*blocks can execute for a singletryblock if the raisedExceptionGroupcontains exceptions matching different clauses. - Automatic Partitioning: Each
except*block receives a newly constructedExceptionGroupcontaining only the matched sub-exceptions, preserving their original nested tree hierarchy. - Automatic Re-raising of Unmatched Errors: Any
exceptions within the group that do not match any
except*clause are automatically bundled into a remainingExceptionGroupand re-raised at the end of the statement. - Syntax Mutual Exclusivity: You cannot mix
exceptandexcept*on the sametryblock.
Through ExceptionGroup and except*, PEP 654
provides Python with native support for composite failures, serving as
the foundational error-handling mechanism for modern concurrency
frameworks like Python 3.11's asyncio.TaskGroup.