async / await and the Event Loop
async/await lets a single thread juggle thousands of I/O operations by cooperatively pausing at await points and resuming when results are ready.
Learning objectives
- › Explain what a coroutine is and how await suspends it
- › Describe the event loop's schedule → run → await → resume cycle
- › Run work concurrently with asyncio.gather and TaskGroup
- › Know when async helps (I/O) and when it doesn't (CPU-bound)
Prerequisites
Version notes
- · `asyncio.run()` is the recommended entry point (3.7+).
- · `asyncio.TaskGroup` (3.11+) gives structured concurrency with clean cancellation.
- · `asyncio.timeout()` context manager arrived in 3.11.
Simple explanation
What it is. async/await is Python's syntax for coroutines — functions that can pause mid-execution and hand control back to an event loop, then resume exactly where they left off once an awaited operation completes.
Why it exists. Threads are heavy and the GIL limits CPU parallelism, but most servers spend their time waiting on the network and disk. A single event loop can interleave thousands of those waits on one thread, cheaply.
The problem it solves. It gives you massive I/O concurrency — thousands of simultaneous connections, API calls, or DB queries — without the memory and context-switch cost of thousands of threads.
How it works. `async def` creates a coroutine. `await x` suspends the coroutine and yields control to the event loop, which runs other ready tasks. When x's underlying I/O completes, the loop resumes the coroutine. It's cooperative: a coroutine only yields at await points, so blocking calls freeze the whole loop.
Analogy: A single chef (the event loop) cooking many dishes. While the pasta boils (awaiting I/O), the chef starts chopping for another order instead of standing idle. Nothing truly happens 'at the same time' — the chef just never waits idly.
Common misconceptions
- "async makes code faster." — It improves concurrency for I/O, not raw CPU speed. CPU-bound code inside a coroutine still blocks the loop.
- "Calling an async function runs it." — It returns a coroutine object; you must await it or schedule it as a task.
- "await means run in parallel." — A bare `await a(); await b()` runs sequentially. Use gather/TaskGroup for concurrency.
Common alternatives
- Threads (ThreadPoolExecutor) for blocking libraries without async support
- Multiprocessing for CPU-bound parallelism
- run_in_executor to offload blocking calls from the loop
Interactive visualization
1async def fetch(name, secs):2 await asyncio.sleep(secs)3 return name45async def main():6 await asyncio.gather(7 fetch("A", 2),8 fetch("B", 1),9 )1011asyncio.run(main())
Event loop tasks
Run main() on the loop
asyncio.run starts the event loop and schedules the main coroutine.
Code examples
import asyncio, time
async def fetch(name, secs):
await asyncio.sleep(secs) # non-blocking wait; loop runs others
return name
async def main():
start = time.perf_counter()
# concurrent: all three waits overlap
results = await asyncio.gather(
fetch("a", 1), fetch("b", 1), fetch("c", 1)
)
print(results, f"{time.perf_counter() - start:.2f}s")
asyncio.run(main())['a', 'b', 'c'] 1.00s
Note: gather schedules all three at once; total time ≈ the slowest, not the sum.
Behind the scenes
- ▸Coroutines are built on the generator machinery: await is analogous to yield, suspending the frame and preserving locals.
- ▸The event loop keeps a queue of ready callbacks and a selector (epoll/kqueue/IOCP) watching file descriptors.
- ▸`await` on a Future registers a callback; when the I/O source fires, the loop schedules the coroutine to resume.
- ▸One thread, one loop: CPU-bound work or blocking syscalls stall every other task until they return.
Interview insights
Key definitions
- · Coroutine: a function defined with async def that can suspend at await and resume later.
- · Event loop: the scheduler that runs ready tasks and resumes coroutines when their awaited I/O completes.
Walk me through what happens when a coroutine hits `await`.
Concise answer: The coroutine suspends and yields control to the event loop, which runs other ready tasks. When the awaited operation completes, the loop resumes the coroutine right after the await.
Detailed answer: await evaluates an awaitable (coroutine/Task/Future). If it isn't done, the coroutine's frame is suspended (its locals preserved, like a paused generator) and control returns to the loop. The loop registers interest in the underlying I/O via its selector and runs other tasks. On completion, the callback marks the Future done and schedules the coroutine to continue.
Common mistake: Saying await 'blocks' — it suspends cooperatively so others run.
Follow-ups:
- · What happens if you await a CPU-bound function?
- · Difference between gather and TaskGroup?
Knowledge check
Does `await a(); await b()` run a and b concurrently?
You call time.sleep(5) inside a coroutine. What happens to the loop?
async/await primarily improves which kind of workload?
Score: 0/3