TRT
Sign in
easyAdvanceddesign

Why is `asyncio.TaskGroup` preferred over `asyncio.gather` in modern Python (3.11+), how does `except*` handle `ExceptionGroup`, and how do you apply.

Contrast `gather`'s orphaned tasks with `TaskGroup`'s scoped cancellation and `finally` joining, explain `except* ValueError as eg:`, and compare `Semaphore` vs bounded `Queue` worker pools.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

Production asyncio enforces two invariants: (1) Bounded Backpressure (asyncio.Semaphore(limit) or asyncio.Queue(maxsize=N)): Never fan out an unbounded input list directly into tasks; acquire a Semaphore(50) inside each worker (or use a fixed pool of 50 worker tasks draining a bounded Queue) so at most 50 requests are in flight at once. (2) Structured Concurrency (async with asyncio.TaskGroup() as tg: in Python 3.11+): Unlike gather, a TaskGroup bounds the lifetime of all child tasks to the lexical async with block. If any child task raises an unhandled exception, the TaskGroup immediately cancels all remaining sibling tasks (by injecting asyncio.CancelledError at their current await point), waits for every cancelled sibling's finally: cleanup block to finish, and only then exits the async with block raising an ExceptionGroup!

No task spawned inside an async with asyncio.TaskGroup() as tg: block is ever still running after execution exits the async with block—whether via normal completion, child exception, or parent cancellation. Because TaskGroup.__aexit__ intercepts the first failing child task, calls .cancel() on every other active child in its set, and awaits their termination before propagating the ExceptionGroup, orphan background tasks are structurally impossible.

Capping fan-out concurrency with asyncio.Semaphore(64) keeps open sockets, TLS buffers, and memory bounded (O(limit) instead of O(N)), while TaskGroup guarantees zero leaked tasks on error unwind. In production systems (AnyIO, Trio (which pioneered Structured Concurrency in Python), FastAPI lifespan/background task groups, and asyncio.timeout(5.0) (Python 3.11+) all build on scoped cancellation and TaskGroup semantics.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how Structured Concurrency (asyncio.TaskGroup), CancelledError, and asyncio.Semaphore backpressure operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Unbounded gather(*10k_coros), TaskGroup + Semaphore(50)) with concrete numbers.
  • Identifies real-world failure modes (Swallowing asyncio.CancelledError in a try ... except BaseException: block without re-raising) and explains how production systems mitigate them.

Common mistakes

  • Treating Structured Concurrency (asyncio.TaskGroup), CancelledError, and asyncio.Semaphore backpressure as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Performing an await inside a finally: block when the task is already being cancelled, without knowing that the await will only work if it doesn't itself get cancelled).
  • Forgetting boundary conditions and error recovery (Spawning 1,000,000 tasks upfront into a TaskGroup guarded by a Semaphore(50)).

This answer walks

Practice follow-ups

  1. 01What happens if you need to run a critical 5-ms database commit inside a coroutine that must NOT be interrupted mid-flight by outer task cancellation (`await asyncio.shield(commit())`)?
  2. 02How does `async with asyncio.timeout(2.0):` (Python 3.11+) work using scoped task cancellation under the hood?

Not started

Sign in to save your learning progress.

Sign in to save