Language Internals
Part 3 of 6 · Python Language Proficiencyasyncio — Event Loop, Tasks, Cancellation & Concurrency Patterns
The asyncio event loop, Tasks, cancellation hygiene, and concurrency patterns on one thread.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does calling an async function return?
Answer
A coroutine object. It does not run until the loop drives it.
L2
What is a Task compared with a coroutine?
Answer
A Task is a scheduled coroutine. You can await it, cancel it, and observe its result.
L3
Where does cancellation land?
Answer
At the next await inside that task, as CancelledError. Code between awaits runs to the next yield.
L4
Why is TaskGroup preferred to a loose gather on 3.11+?
Answer
The group is the lifetime. A failure cancels the siblings and the exit waits for that cancellation to finish.
L5
How do you keep 50 fetches from opening 50 sockets at once?
Answer
An asyncio.Semaphore around the I/O, or a bounded queue of workers.
L6
How do you call a blocking library without stalling the loop?
Answer
asyncio.to_thread or a loop executor. Do not call it directly in the coroutine.
L7
What does shield do, and why is it rare?
Answer
It protects an inner await from outer cancellation. The outer task still ends cancelled. Use it only for a short critical section you must finish.
Failure modes
Blocking the loop
time.sleep, sync requests, or a CPU loop prevents every other task from running.
Swallowed CancelledError
A bare except or except BaseException logs CancelledError and continues, so shutdown hangs.
Orphaned tasks
create_task without keeping the Task lets the job disappear or crash unseen.
Unbounded fan-out
gather of thousands of connects overwhelms the peer and the file-descriptor limit.
Misconceptions
asyncio runs Python bytecode in parallel.
One thread runs bytecode. Overlap is in the waits.
gather returns results in completion order.
Results line up with the input order. Completion order is a different API.
async with is just with inside an async function.
An async context manager can await in __aenter__ and __aexit__. A plain with cannot.
Interviewer traps
Drawing a thread pool when asked what await does.
One thread. The coroutine suspends. The loop runs someone else who is ready.
Catching Exception around an await during shutdown.
CancelledError inherits BaseException on current Python. Catch it explicitly and re-raise.
Equating this loop with the JavaScript microtask queue in detail.
Both are single-threaded and evented. Python await points are explicit. Leave the JS queue lesson on the JS page.
Design scenario
Same prompt for every reader.
Requirements
Structured tasks, a semaphore of 10, a timeout, and cleanup that still cancels siblings.
Traffic / scale
Hundreds of handlers, each with dozens of in-flight calls.
Latency
The budget is two seconds for the fan-out, not for each call stacked serially.
Consistency
A cancelled handler does not record a success for a call that was aborted.
Availability
One blocking call must not freeze the other handlers.
Failure assumptions
- The HTTP client is synchronous.
- CancelledError is logged and ignored.
- Tasks are created and not retained.
Constraints
- Stay on one event loop. The GIL page owns process pools.
- Name the nursery, the limit, and the timeout.
Prompt
A client handler fans out to 50 HTTP calls and must stop them when the client disconnects or a two-second budget expires.
Overview
asyncio overlaps waiting. A coroutine runs until await, then the loop can run another ready coroutine. If you never await, or you call a blocking API, every other task waits behind you.
Node is also one thread of evented I/O. The JavaScript event loop splits microtasks and macrotasks. Python makes the yield points explicit with await. The JS/TS hub is the sibling map, not this lesson.
Loop, ready queue, waiters
Flow
- 1
Event loop
- nextReady queue
- 2
Ready queue
- nextTask runs until await
- 3
Task runs until await
- nextWaiters: I/O or timers
- 4
Waiters: I/O or timers
- readyReady queue
Lesson map
asyncio — Event Loop, Tasks, Cancellation & Concurrency Patterns
The asyncio event loop, Tasks, cancellation hygiene, and concurrency patterns on one thread.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB loop["Event loop"] ready["Ready queue"] run["Task runs until await"] wait["Waiters: I/O or timers"] loop -->|Event loop to Ready queue| ready ready -->|Ready queue to Task runs until await| run run -->|Task runs until await| wait wait -->|ready| ready
| Object | Role |
|---|---|
| Coroutine | The object async def returns. Not running yet |
| Task | A scheduled coroutine you can cancel or await |
| Future | A low-level result that something will set |
| Event loop | Scheduler plus the I/O multiplexer |
| TaskGroup (3.11+) | A nursery. Failure cancels the siblings |
import asyncio
async def fetch(i, sem):
async with sem:
await asyncio.sleep(0.05)
return f"ok-{i}"
async def main():
sem = asyncio.Semaphore(10)
async with asyncio.timeout(2.0):
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(i, sem)) for i in range(50)]
return [t.result() for t in tasks]The page runner records the same rules without opening sockets. A timeout budget wraps the nursery. The semaphore caps how many fetches sit in await at once. If one task fails, the group cancels the rest.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Cancellation
task.cancel() injects CancelledError at the next await. Since Python 3.8 that error subclasses BaseException, not Exception, so except Exception does not catch it. A bare except or except BaseException can swallow it. Clean up, then re-raise.
asyncio.shield keeps an inner await running when the outer task is cancelled. The outer cancellation still happens. Use shield for a short section you cannot abandon, not as a default.
Press Run. Snippets must be self-contained — no network, files, or native modules.
async def careful_work():
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
await asyncio.sleep(0)
raisePatterns
| Pattern | Use |
|---|---|
asyncio.gather | Fan-out when you want every result. Set return_exceptions on purpose |
TaskGroup | Structured lifetime on 3.11 and later |
Semaphore | Cap in-flight I/O |
Queue | Producer and consumer pipelines |
to_thread | Blocking libraries leave the loop |
timeout / wait_for | A budget on every external call |
asyncio and threads for I/O
| asyncio | threads | |
|---|---|---|
| Style | Cooperative | Preemptive |
| Many sockets | Cheap | OS threads and stack memory |
| Libraries | Needs an async API | Sync libraries are fine |
| Cancellation | First-class on Tasks | Flags, or a hard kill |
| Debugging | Stacks across awaits | Thread dumps |
The GIL page is the choice between these and processes. This page assumes you already picked the loop.
Interview Q&A
What actually runs concurrently?
Answer
Waiting. One thread still executes Python. Two coroutines overlap only while one of them is suspended at await.
async with versus with?
Answer
async with calls __aenter__ and __aexit__, which may await. with cannot suspend. Async locks and sessions need the async form.
How do you run blocking disk or DNS work?
Answer
asyncio.to_thread, or loop.run_in_executor. The coroutine awaits that thread. The loop stays free.
Why keep a reference to a Task?
Answer
The loop only holds a weak reference in some paths. A task you drop can be collected mid-flight, and its exception becomes invisible.
Does gather reorder results?
Answer
No. The result list matches the order of the awaitables you passed, even if the last one finished first.
What is wrong with time.sleep in a coroutine?
Answer
It blocks the OS thread. No other task runs until it returns. Use asyncio.sleep.
Shield versus cancel?
Answer
Cancel asks the task to stop at the next await. Shield wraps an inner await so that particular wait is not the cancellation point. The caller is still cancelled.
How is this different from the JavaScript event loop?
Answer
Both are one thread. JS queues microtasks ahead of timers. Python runs until an explicit await. Read the JS page for queue order. Stay here for TaskGroup and CancelledError.
Pitfalls
time.sleep, heavy CPU, or sync HTTP insideasync def.create_taskwithout retaining the task.- Sharing a non-thread-safe object with
to_threadand hoping for the best. - Treating gather's result order as completion order.
- Swallowing
CancelledError.
A handler awaits three calls and the event loop stops logging. List the three APIs that would freeze it if someone used the sync version, and the one-line replacement for each.