Skip to content

Cancellation and TaskGroup

The quality of async code is shaped less by how much it can run concurrently and more by how well it handles cancellation and cleanup. `CancelledError` is part of control flow, and `TaskGroup` gives that control flow a structured home.

Quick takeaway: cancellation is modeled through exception propagation. If cleanup is required, perform it in `finally` or under `except asyncio.CancelledError`, then usually re-raise. For groups of tasks, `TaskGroup` is the safest default.

Picture the Structured-Concurrency Flow

When a timeout or child-task failure happens, TaskGroup cancels sibling tasks, lets them clean up, and then reports remaining errors structurally.

Code Pattern for Proper Cancellation

py
import asyncio


async def worker(name: str) -> None:
    try:
        while True:
            print(f"{name}: tick")
            await asyncio.sleep(0.2)
    except asyncio.CancelledError:
        print(f"{name}: cleanup")
        raise


async def main() -> None:
    try:
        async with asyncio.timeout(0.6):
            async with asyncio.TaskGroup() as task_group:
                task_group.create_task(worker("alpha"))
                task_group.create_task(worker("beta"))
    except TimeoutError:
        print("timeout reached")


asyncio.run(main())

`asyncio.timeout()` surfaces a `TimeoutError` outside the block, but internally it cancels the running work. Each worker must clean up and re-raise `CancelledError` so structured cancellation continues to behave correctly.

Why Swallowing CancelledError Is Dangerous

  • the parent loses the cancellation signal
  • graceful shutdown sequencing becomes unreliable
  • timeout behavior can turn into hanging behavior

What TaskGroup Gives You

  • task lifetime tied to a lexical block
  • sibling cancellation on failure
  • grouped exceptions through ExceptionGroup

Checklist

Cleanup in finally

Files, sockets, semaphores, and consumers should usually clean up in `finally` blocks.

Re-raise cancellation

Unless you have a very specific reason, do not swallow `CancelledError`.

Prefer TaskGroup

For new code, `TaskGroup` is usually safer than scattered `create_task()` calls.

Design for timeout

Timeout paths are part of the contract, especially in fan-out service calls.

Official Sources

Built with VitePress for a Python 3.14 handbook.