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:
- The task sets an internal
_must_cancelflag - If the task is waiting on a Future,
CancelledErroris thrown viacoro.throw() - The error propagates up through the coroutine’s call stack
- If uncaught, the Task marks itself as cancelled
task.cancelled()returnsTrue
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.
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.