TaskGroup and structured concurrency

asyncio · Practical · 12 min

TaskGroup removes leaked tasks and lost errors, the two classic asyncio bugs.

asyncio.TaskGroup (3.11+) is structured concurrency: tasks started in a group cannot outlive the async with block that owns them.

async with asyncio.TaskGroup() as tg:
    t1 = tg.create_task(fetch(1))
    t2 = tg.create_task(fetch(2))
# here both are finished

Leaving the block waits for all tasks. If any task raises, the group cancels the remaining tasks, waits for them to finish, and then raises an ExceptionGroup containing every exception that wasn't a cancellation. Handle it with except*, which matches exceptions inside the group by type:

except* ValueError as eg:
    for e in eg.exceptions: ...

Compared with gather, a TaskGroup never leaves orphan tasks running after a failure, holds strong references to its tasks, and reports all errors, not just the first. It is the right default for "do these N things concurrently".

Results are read from the task objects after the block: t1.result(). To cap concurrency inside a group, combine it with a Semaphore. The 3.13 docs also show a pattern for ending a group early: raise a private exception from a task and catch it with except* outside.

Example

import asyncio

async def work(n):
    await asyncio.sleep(0.1 * n)
    if n == 2:
        raise ValueError(f"task {n} failed")
    print(f"task {n} finished")
    return n

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(work(n)) for n in (1, 2, 3)]
    except* ValueError as eg:
        print("errors:", [str(e) for e in eg.exceptions])
    print([t.cancelled() for t in tasks])

asyncio.run(main())
# task 1 finished
# errors: ['task 2 failed']
# [False, False, True]   (task 3 was cancelled)

Remember

  • Tasks in a TaskGroup are all done when the async with block exits
  • One failure cancels the siblings
  • Errors arrive as an ExceptionGroup; handle with except*

Watch out for

  • Using plain except ValueError around a TaskGroup; the group raises ExceptionGroup, so use except*.

Check yourself

  1. In a TaskGroup, task A raises while B is still sleeping. What happens to B?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/asyncio-task.html#task-groups