TaskGroup Structured Concurrency
TaskGroup Structured Concurrency
Thesis
asyncio.TaskGroup (3.11+) is structured concurrency for asyncio: tasks spawned in the group are awaited together; the first unhandled exception cancels siblings and re-raises as ExceptionGroup. Lifetimes nest with the async with block.
Mental model
async with asyncio.TaskGroup() as tg:
tg.create_task(work(1))
tg.create_task(work(2))
# all done or cancelled; exceptions bundled
Contrast: bare create_task without a supervisor leaks failures into “Task exception was never retrieved”.
Worked example
# taskgroup_demo.py
import asyncio
async def ok(n):
await asyncio.sleep(0.05)
return n * 2
async def boom():
await asyncio.sleep(0.01)
raise RuntimeError("boom")
async def main_ok():
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(ok(1))
t2 = tg.create_task(ok(2))
print("results", t1.result(), t2.result())
async def main_fail():
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(ok(1))
tg.create_task(boom())
except* RuntimeError as eg:
print("caught", type(eg).__name__, [repr(e) for e in eg.exceptions])
asyncio.run(main_ok())
asyncio.run(main_fail())results 4 9
ExceptionGroup: ExceptionGroup ['boom-1']
Trap
Swallowing the ExceptionGroup without inspecting .exceptions. Also: spawning tasks that outlive the group on purpose — that is the opposite of structured concurrency; use an explicit long-lived supervisor if you must.
Boring rule
Prefer TaskGroup (or Trio nurseries) whenever several async jobs share a scope. Use except* to unwrap typed failures.
Try this
- Add a third task that ignores
CancelledErrorincorrectly and observe shutdown. - Nest TaskGroups and cancel the outer scope.
- Compare with
asyncio.gather(..., return_exceptions=True).
Sources
- Python docs: TaskGroup
- PEP 654 — Exception Groups
- PEP 678 — Exception Notes