Python 3.11 TaskGroups and Structured Concurrency
Python 3.11 introduced asyncio.TaskGroup, an
asynchronous context manager that establishes structured concurrency
within the standard library. This article covers what
TaskGroup is, the problems it solves compared to older APIs
like asyncio.gather(), how it manages task lifecycles, and
how it handles errors using Python 3.11's
ExceptionGroup.
What is Structured Concurrency?
Structured concurrency is a programming paradigm where concurrent operations have a well-defined entry point, exit point, and lifespan tied to a specific execution scope. In traditional asynchronous programming, concurrent tasks can easily become "orphaned"—continuing to run in the background, consuming resources, and producing silent failures if their caller exits early due to an error.
Under structured concurrency, a parent execution block cannot finish until all of its spawned child tasks have finished. If any child task fails, the remaining sibling tasks are cleanly cancelled and cleaned up before control returns to the caller.
The Problem with
asyncio.gather()
Prior to Python 3.11, the primary tool for managing concurrent tasks
was asyncio.gather(). While functional,
asyncio.gather() suffers from several pitfalls:
- Orphaned Tasks: By default, if one task raises an
exception,
asyncio.gather()immediately raises that exception to the caller, leaving sibling tasks running in the background detached from the main control flow. - Complex Cancellation: Ensuring that sibling tasks are cancelled and awaited upon failure requires boilerplate code and custom wrappers.
- Lost Exceptions: When multiple tasks fail
concurrently,
asyncio.gather()typically surfaces only the first failure, discarding or masking subsequent errors.
How asyncio.TaskGroup
Works
asyncio.TaskGroup enforces structured concurrency using
an asynchronous context manager (async with). Tasks are
spawned inside the context block using tg.create_task().
When the context manager exits, it automatically awaits all tasks
spawned within it.
import asyncio
async def fetch_data(id: int, delay: int):
await asyncio.sleep(delay)
return f"Data {id}"
async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_data(1, 1))
task2 = tg.create_task(fetch_data(2, 2))
# Both tasks are guaranteed to be complete here
print(task1.result())
print(task2.result())
asyncio.run(main())In this model, the context manager block will not exit until
task1 and task2 have completed execution.
Error Handling and
ExceptionGroup
The core advantage of TaskGroup is deterministic error
handling:
- Automatic Cancellation: If a task raises an
unhandled exception,
TaskGroupimmediately cancels all other active tasks within its scope. - Graceful Teardown: It waits for all cancelled tasks to finish their cancellation routines before exiting the context block.
- Aggregated Errors: Instead of discarding errors,
TaskGroupcollects all exceptions from failed tasks into anExceptionGroup.
Python 3.11 also added the except* syntax specifically
to catch and handle sub-exceptions within an
ExceptionGroup:
import asyncio
async def faulty_task():
await asyncio.sleep(0.5)
raise ValueError("Something went wrong")
async def slow_task():
try:
await asyncio.sleep(2)
except asyncio.CancelledError:
print("Slow task was cancelled safely")
raise
async def main():
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(faulty_task())
tg.create_task(slow_task())
except* ValueError as eg:
print(f"Handled validation error: {eg.exceptions}")
asyncio.run(main())When faulty_task raises a ValueError,
TaskGroup cancels slow_task, waits for it to
handle the cancellation, and packages the ValueError into
an ExceptionGroup caught by
except* ValueError.
Key Takeaways
asyncio.TaskGroup represents the modern, recommended
approach for managing concurrent tasks in Python:
- Tasks are strictly bound to the scope of an
async withblock. - Parent blocks always wait for all children to complete.
- Unhandled errors trigger immediate, safe cancellation of sibling tasks.
- Multiple simultaneous errors are safely grouped and handled via
ExceptionGroupandexcept*.