Exception Groups, except* & Structured Concurrency
When running concurrent tasks (via asyncio or worker pools), multiple operations can fail simultaneously. Prior to Python 3.11, reporting multiple concurrent errors required swallowing or picking an arbitrary single exception to raise. Introduced in PEP 654 and PEP 654/655, Exception Groups (ExceptionGroup), except* filtering syntax, and asyncio.TaskGroup provide structured concurrency and multi-exception handling.
This chapter details ExceptionGroup architecture, except* partial exception matching syntax, BaseExceptionGroup, and asyncio.TaskGroup structured concurrency.
1. Exception Groups Architecture (ExceptionGroup / PEP 654)
An ExceptionGroup is an exception containing a tree of nested exception instances:
ExceptionGroup Structural Tree:
[ ExceptionGroup: "Multiple task failures" ]
βββ [ ValueError: "Invalid user ID" ]
βββ [ ExceptionGroup: "Sub-task failures" ]
βββ [ ConnectionError: "Database timeout" ]
βββ [ KeyError: "Missing auth token" ]# Instantiating an ExceptionGroup manually
eg = ExceptionGroup(
"Batch processing failed",
[ValueError("Invalid ID"), ConnectionError("DB Timeout")]
)
raise eg2. Partial Exception Filtering via except* Syntax
Standard except ValueError: can only catch a single top-level exception. Handling an ExceptionGroup containing heterogeneous exception types requires except* syntax:
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_user()) # Raises ValueError
tg.create_task(fetch_db()) # Raises ConnectionError
except* ValueError as eg:
# Catches ONLY the ValueError instances from the ExceptionGroup!
print(f"Handled ValueErrors: {eg.exceptions}")
except* ConnectionError as eg:
# Catches ONLY the ConnectionError instances from the ExceptionGroup!
print(f"Handled ConnectionErrors: {eg.exceptions}")except* Matching Invariants:
- Partial Splitting:
except* Tsplits the matching exception instances of typeTinto a new sub-ExceptionGroup, leaving un-matched exceptions in the original group. - Multiple Branch Execution: Multiple
except*blocks can execute for a singletryblock if theExceptionGroupcontains types matching multiple branches!
3. Structured Concurrency with asyncio.TaskGroup
Introduced in Python 3.11, asyncio.TaskGroup is an asynchronous context manager that guarantees Structured Concurrency:
import asyncio
async def main():
try:
async with asyncio.TaskGroup() as tg:
# Spawn concurrent tasks
task1 = tg.create_task(fetch_api_a())
task2 = tg.create_task(fetch_api_b())
except* TimeoutError:
print("One or more API tasks timed out!")Structured Concurrency Invariants:
- Guaranteed Exit Joining: When exiting the
async with TaskGroup()block, execution pauses until all child tasks have completed. - Automatic Cancellation: If any task raises an unhandled exception,
TaskGroupautomatically cancels all remaining active child tasks. - Combined Exception Reporting: All task exceptions are aggregated into an
ExceptionGroupand raised together.
4. Production Trade-offs: TaskGroup vs gather()
asyncio.gather(*tasks): Legacy task aggregator. If one task fails, remaining tasks continue running in the background untracked (leaking dangling tasks) unless explicitly cancelled.asyncio.TaskGroup(): Modern structured concurrency. Eliminates dangling background tasks by enforcing deterministic cleanup and cancellation.