Python Central HubSign in

Python Programming / Python Asyncio

Structured Concurrency with TaskGroup

Use asyncio.TaskGroup (Python 3.11+) for safer concurrent task lifecycles and error handling.

Why TaskGroup?

asyncio.gather is useful, but it can make lifecycle and error handling messy.

asyncio.TaskGroup (Python 3.11+) provides structured concurrency:

  • tasks are scoped to a block
  • if one task fails, the group cancels the rest
  • errors are grouped in an ExceptionGroup

Example

Python
import asyncio
 
 
async def job(name: str, delay: float):
    await asyncio.sleep(delay)
    return f"{name} done"
 
 
async def main():
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(job("A", 0.2))
        t2 = tg.create_task(job("B", 0.1))
 
    # task results are available after the block
    print(t1.result())
    print(t2.result())
 
 
asyncio.run(main())

Handling failures

Python
import asyncio
 
 
async def ok():
    await asyncio.sleep(0.1)
    return 1
 
 
async def bad():
    await asyncio.sleep(0.05)
    raise ValueError("boom")
 
 
async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(ok())
            tg.create_task(bad())
    except* ValueError as eg:
        # ExceptionGroup support (PEP 654)
        print("handled:", eg)
 
 
asyncio.run(main())
sketch When one task fails: gather leaves the others running, TaskGroup does not p5.js
Three workers; the middle one raises. Both forms report the error, but they leave the program in different states. gather returns the exception and the surviving sibling carries on to completion -- checked afterwards, it reports done and not cancelled. TaskGroup cancels every sibling before it re-raises, wrapped in an ExceptionGroup. That is what structured concurrency means: the block does not finish while anything it started is still running.

Notes

  • Requires Python 3.11+
  • Consider TaskGroup for “start N tasks, all must finish” patterns

The idea in one sentence

Structured concurrency means tasks live inside a block of code, and the block does not finish until every task it started has finished. Nothing is left running in the background by accident.

Reading the examples

In taskgroup_basic.py, two tasks are created with tg.create_task(...) inside async with asyncio.TaskGroup(). The async with block ends only after both tasks are complete, which is why t1.result() and t2.result() are safe to call right after it. Task B has the shorter delay and finishes first, but the block still waits for A.

In taskgroup_error.py, bad() raises after 0.05 seconds while ok() would need 0.1. The group notices the failure, cancels ok(), waits for it to stop, and then raises an ExceptionGroup containing the ValueError. The except* syntax catches errors of a given type out of that group. It exists because several tasks can fail at the same time, and all of their errors are reported together.

Comparing with gather

With asyncio.gather, one failing task raises its error to you, but the other tasks keep running unless you cancel them yourself. That is how leaked tasks happen. A TaskGroup cannot leak: leaving the block means everything has stopped.

A practical example

Python
import asyncio
 
 
async def square(n: int) -> int:
    await asyncio.sleep(0.05)
    return n * n
 
 
async def main() -> None:
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(square(n)) for n in range(5)]
    print([t.result() for t in tasks])  # [0, 1, 4, 9, 16]
 
 
asyncio.run(main())

Common mistakes

  • Reading result() inside the block. The tasks may not be finished yet. Read results after the async with ends.
  • Using except ValueError instead of except* ValueError. The error arrives wrapped in an ExceptionGroup, so a plain except will not match.
  • Running on Python older than 3.11. TaskGroup and except* do not exist there; use gather instead.
  • Catching and hiding errors inside every task. The group can only cancel siblings if the error escapes the failing task.

When to use it

Use a TaskGroup for "start these N jobs and they all must succeed" work: fetching several pieces of data for one page, or running the steps of one request. Use gather(..., return_exceptions=True) when you want every outcome, including failures, without cancelling anything.

🧪 Try It Yourself

Exercise 1 – Create a Task

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Gather Multiple Tasks

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Task Name and Done

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did