Python Task Cancellation — Core Concepts

How Cancellation Works

Calling task.cancel() doesn’t immediately stop the task. It schedules a CancelledError to be thrown into the coroutine at its next await point. The task is only truly cancelled when that error propagates up uncaught.

import asyncio

async def slow_work():
    try:
        await asyncio.sleep(3600)  # CancelledError lands here
    except asyncio.CancelledError:
        print("Cleaning up...")
        raise  # IMPORTANT: re-raise to actually cancel

async def main():
    task = asyncio.create_task(slow_work())
    await asyncio.sleep(1)
    task.cancel()  # Request cancellation
    try:
        await task
    except asyncio.CancelledError:
        print("Task was cancelled")

The CancelledError Journey

When task.cancel() is called:

  1. The task sets an internal _must_cancel flag
  2. If the task is waiting on a Future, CancelledError is thrown via coro.throw()
  3. The error propagates up through the coroutine’s call stack
  4. If uncaught, the Task marks itself as cancelled
  5. task.cancelled() returns True

Catching and Re-raising

You can catch CancelledError for cleanup, but you must re-raise it unless you have a very good reason not to:

async def download(url):
    conn = await connect(url)
    try:
        return await conn.read()
    except asyncio.CancelledError:
        await conn.close()  # Clean up the connection
        raise               # Let cancellation propagate

Swallowing CancelledError (not re-raising) makes the task appear to complete normally — the caller waiting on it won’t know cancellation was requested.

cancel() Can Fail

task.cancel() returns True if the task was successfully asked to cancel, False if it was already done. But even a True return doesn’t guarantee the task will be cancelled — the coroutine might catch and suppress the error.

async def stubborn():
    while True:
        try:
            await asyncio.sleep(1)
        except asyncio.CancelledError:
            print("Not today!")  # Swallows cancellation — bad practice
            continue

Cancellation Messages (Python 3.9+)

You can pass a message to cancel() for debugging:

task.cancel(msg="User timeout exceeded")
# Later, the CancelledError carries this message

Shielding from Cancellation

Sometimes an inner operation must complete even if the outer task is cancelled. asyncio.shield() protects it:

async def transfer_money(amount):
    # This MUST complete — don't cancel mid-transfer
    result = await asyncio.shield(execute_transfer(amount))
    return result

Shield creates a wrapper future. If the outer task is cancelled, the shield is cancelled but the inner coroutine keeps running. However, the outer coroutine still gets CancelledError — shield doesn’t prevent the caller from being cancelled.

Common Misconception: “cancel() Stops the Task Immediately”

It doesn’t. The task continues running until it hits an await. If your task is doing CPU-intensive work in a tight loop without any await calls, it won’t notice the cancellation. This is by design — cooperative cancellation prevents data corruption from mid-operation interrupts.

Timeout-Based Cancellation

The most common use of cancellation is through timeouts:

async def main():
    try:
        # Cancels the inner coroutine after 5 seconds
        async with asyncio.timeout(5):
            await slow_operation()
    except TimeoutError:
        print("Operation timed out")

asyncio.timeout() (Python 3.11+) creates a deadline and cancels the task if it’s exceeded. Under the hood, it uses task.cancel().

Cleanup with try/finally

The most reliable cleanup pattern uses finally, which runs whether the coroutine completes, raises, or is cancelled:

async def managed_connection():
    conn = await open_connection()
    try:
        await process(conn)
    finally:
        await conn.close()  # Runs even on cancellation

One thing to remember: Cancellation is cooperative — task.cancel() throws CancelledError at the next await, and well-written code catches it only for cleanup before re-raising.

pythonconcurrencyasynciocancellation

See Also

  • Python Actor Model Why treating each piece of your program like a person with their own mailbox makes concurrency way less scary.
  • Python Aiocache Caching aiocache remembers expensive answers so your async Python app doesn't waste time asking the same question twice.
  • Python Aiofiles Async Io aiofiles lets your async Python program read and write files without freezing — because normal file operations secretly block everything.
  • Python Aiohttp Understand Aiohttp through an everyday analogy so Python behavior feels intuitive, not random.
  • Python Anyio Portability AnyIO lets your async Python code work with any async library — write once, run on asyncio or Trio without changes.