asyncio Introduction
asyncio is Python's built-in framework for writing concurrent code with a single thread and an event loop. Instead of creating a thread per task, you write coroutines - functions that can pause and resume - and the event loop schedules thousands of them on one thread, switching at every await. The payoff is massive I/O concurrency with tiny memory overhead: a server handling 10,000 open connections does not need 10,000 threads.
The Event Loop
The event loop is a scheduler. It keeps a set of ready coroutines and I/O watchers; when a coroutine awaits I/O, the loop parks it and runs the next ready one; when the I/O completes, the loop resumes the parked coroutine. Everything - coroutines, I/O callbacks, timers - runs on the same thread, in order. There are no data races from thread interleaving, but a single slow synchronous call can stall every task in the loop.
First Example
import asyncio
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
# three tasks overlap: total time ~3s, not 6s
await asyncio.gather(
say_after(1, "one"),
say_after(2, "two"),
say_after(3, "three"),
)
asyncio.run(main())
asyncio.run(main()) creates a fresh event loop, runs main to completion, and then closes the loop - it is the standard entry point for an asyncio program.
Sync vs Async, Side by Side
import time
import asyncio
import requests
def sync_fetch(urls):
for u in urls:
requests.get(u) # each call blocks the thread
async def async_fetch(urls):
import aiohttp
async with aiohttp.ClientSession() as session:
await asyncio.gather(*(session.get(u) for u in urls)) # overlap
The sync version spends its life waiting; the async version waits without blocking, so all requests fly out almost at once.
The Two Rules
- Inside an event loop, never call a blocking function (
time.sleep,requests.get, synchronous DB drivers) - it freezes every other task. Use the async equivalents (asyncio.sleep,aiohttp, async drivers) or offload blocking work withasyncio.to_thread. - Await coroutines. Calling an
async defwithoutawaitcreates a coroutine object that runs only if awaited; Python warns about coroutines that are never awaited.
Summary
asyncio gives you I/O concurrency on one thread: write coroutines, await every suspending call, and run everything through asyncio.run. It is the right model when hundreds or thousands of connections wait on the network, and the wrong model when the work is CPU-heavy.