TaskGroup vs gather

Concurrency in Python services Senior Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

One of two tasks fails. What's printed?

Python 3.13
import asyncio


async def ok(log: list[str]) -> None:
    try:
        await asyncio.sleep(1)
        log.append("ok finished")
    except asyncio.CancelledError:
        log.append("ok cancelled")
        raise


async def bad() -> None:
    raise ValueError("boom")


async def main() -> None:
    log: list[str] = []
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(ok(log))
            tg.create_task(bad())
    except* ValueError:
        log.append("caught")
    print(log)


asyncio.run(main())

Options
Hint 1 of 1

What does a TaskGroup do to siblings when one fails?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A ['ok cancelled', 'caught']

Correct. The group cancels the sibling, then raises an ExceptionGroup that except* catches.

When one task in a TaskGroup fails, the group cancels the others and waits for them before leaving. ok logs its cancellation and re-raises, then the group raises an ExceptionGroup that except* matches.

Why not the others?
  • B ['ok finished', 'caught']. TaskGroup cancels siblings on failure.
  • C ['caught']. The cancelled sibling logs before re-raising.
  • D It crashes with ValueError. except* ValueError handles it.

30-second answer

Both run coroutines concurrently. With gather, if one fails the exception propagates but the others keep running in the background unless you cancel them, and return_exceptions=True mixes errors into results. TaskGroup (Python 3.11+) is structured: when one task fails, it cancels the rest, waits for them, and raises an ExceptionGroup with every failure. No task outlives the async with block, so nothing leaks.

Practise saying it · 30 s. No recording, just a timer

TaskGroup or gather?

Say your answer out loud, then check which of these points you covered:

  • Both concurrent
  • TaskGroup cancels siblings
  • ExceptionGroup, except*
  • No leaked tasks

Deep dive

Handle ExceptionGroup with except* clauses.

Use gather with return_exceptions=True when you want all results regardless of failures.

Tasks created with bare create_task need a reference kept, or they can be garbage-collected mid-run.

asyncio.timeout around a TaskGroup bounds the whole group.

Common mistakes

  • Fire-and-forget create_task with no reference.
  • gather where one failure leaves siblings running unnoticed.
  • Catching ExceptionGroup with a plain except Exception and losing details.

Senior take & follow-ups

Compare failure behaviour, cancellation and leaks; recommend TaskGroup by default.

Follow-up questions

Think of your answer first, then open the hint.

  1. What does except* do?

    Show hint

    Matches exceptions inside an ExceptionGroup by type.

  2. Why keep task references?

    Show hint

    The loop holds only weak references to tasks.

Suggest a fix if something here is wrong or out of date.