Asyncio TaskGroup, ExceptionGroups, and Cooperative Cancel Scopes
- Last updated
- Prerequisites:
- Python async/await coroutines
- Asyncio event loop basics
- python
- asyncio
- taskgroup
- structured-concurrency
- exceptiongroup
- cancellation
Read at your depth
The practical view
`asyncio.TaskGroup` (introduced in Python 3.11) brings **Structured Concurrency** to Python, replacing `asyncio.gather()` and raw `asyncio.create_task()`. Using `async with asyncio.TaskGroup() as tg:`, you spawn tasks with `tg.create_task(coro())`. If any child task raises an exception, the TaskGroup automatically cancels all remaining running sibling tasks and groups all errors into an `ExceptionGroup` (handled using `except*`).
The same idea in other frameworks
`async with asyncio.TaskGroup() as tg: tg.create_task(...)` with `except*`.Java 21 Project Loom `StructuredTaskScope.ShutdownOnFailure()`.Tokio `JoinSet` or `futures::future::try_join_all`.`Promise.all()` (without automatic sibling cancellation).Legacy vs modern
asyncio.gather Orphan Leaks vs Structured asyncio.TaskGroup
`asyncio.gather` leaves sibling tasks running when one fails; `TaskGroup` cleanly cancels siblings and aggregates exceptions.
await asyncio.gather(fetch_a(), fetch_b()); // ❌ If B fails, A continues running orphaned in backgroundasync with asyncio.TaskGroup() as tg: tg.create_task(a()); tg.create_task(b()); // ✅ NEW: Auto-cancels siblingsInterview gotchas
Context
Interviewers test your knowledge of Python 3.11+ exception group pattern matching.
The mechanical answer
Standard `except ValueError` cannot match inside an `ExceptionGroup` containing multiple disparate errors. **`except* ValueError as eg`** matches and extracts all matching `ValueError` exceptions from the group, allowing other handlers (e.g. `except* TypeError`) in the same `try/except*` block to simultaneously match and handle their respective errors from the same composite exception tree.
Trap
You cannot mix traditional `except` and modern `except*` in the same `try` statement.
Context
Checks knowledge of cooperative cancellation contracts in Python asyncio.
The mechanical answer
If a task catches `asyncio.CancelledError` and continues executing without re-raising or returning, the `TaskGroup`'s `async with` block **will hang indefinitely** because `TaskGroup.__aexit__` strictly waits for every registered child task to finish before exiting.
Trap
When catching `asyncio.CancelledError` for cleanup, always re-raise `raise` or return immediately after cleaning up resources.