5.2Asynchronous programming (asyncio)
The essentials in 30 seconds
- Async = exploiting waiting: while it waits (API, database, files), the program does something else instead of staying blocked.
- The trio: async def declares, await waits without blocking, gather launches together — asyncio.run at the top.
- Separate worlds: blocking code (requests, sleep) paralyses async — async-compatible libraries or nothing.
Running mini-case — Nabil speeds up his collections in Algiers
Nabil, junior data analyst in Algiers, fetches data every morning from several web sources: sequentially, his script waits for each response one after another and the collection takes forever. He rewrites the loop as coroutines launched together with asyncio.gather: waits overlap instead of adding up. A follow-up to case 5.1 — Nabil occupies idle time instead of enduring it.
Follow-up to case 5.1 — Nabil moves from sequential waiting to overlapping concurrency.
1. When waiting dominates: async
APIs, databases, remote files: the program waits more than it computes. Asynchronous programming frees the thread during the wait: instead of staying blocked, it moves on to other tasks, and a single thread handles thousands of concurrent requests without thousands of threads. Nabil recognises his morning collection there: the bottleneck is network waiting, not computation.
Clear boundary: for pure computation (CPU-bound), async speeds up nothing — that is multiprocessing. Async for I/O, multiprocessing for CPU: choose by the bottleneck, not by fashion.
2. The trio: async def, await, gather
async def fetch(url) declares a coroutine, await waits without blocking (the thread is freed for other tasks), asyncio.gather(*tasks) launches together and waits for everyone. asyncio.run(main()) orchestrates from the top via the event loop. Nabil launches his collections with gather: three waits overlap instead of adding up.
Classic trap: forgetting an await leaves a coroutine never executed (a quiet warning, an empty result). Every created coroutine must be awaited or gathered.
3. Do not mix worlds
Blocking code inside async paralyses everything: time.sleep freezes the thread, requests (synchronous) cancels the benefit — use asyncio.sleep and async-compatible libraries (httpx, aiohttp). A leftover blocking call is isolated via run_in_executor. Nabil replaces requests with an async client: without that, his gather kept waiting anyway.
Golden rule: async end to end or not at all — the mix is the worst of both worlds. And measure before/after to prove the gain instead of assuming it.
Did you know?
Coroutine created, never awaited: work promised, never done.
🔎 Concrete example — Nabil collects his sources in parallel
Waits overlap instead of adding up.
import asyncio
async def collecter(source):
print(f"collecte {source}...")
await asyncio.sleep(1)
return f"{source} : ok"
async def main(sources):
return await asyncio.gather(*(collecter(s) for s in sources))
res = asyncio.run(main(["api", "fichier", "base"]))
print(res)
Reading: three one-second waits take about one second in total, not three.
Lesson: in network production, replace the sleep with an async client (httpx/aiohttp).
Section vocabulary
- Asynchronous programming
- A model that keeps the program busy during waiting time instead of blocking it.
Simple example: Nabil collects three sources at once instead of awaiting each.
What it is not: several processors computing in parallel.
Reflex: does my program wait more than it computes?
- async / await
- async declares a coroutine; await waits without blocking the thread.
Simple example: Nabil awaits each collection to free the thread.
What it is not: a blocking call that freezes everything.
Reflex: did I forget an await (ghost coroutine)?
- asyncio.gather
- Launches several coroutines together and waits for all of them.
Simple example: Nabil gathers his three collections in a single gather.
What it is not: a loop awaiting each task one after another.
Reflex: can these waits overlap?
- Coroutine
- An async function that the event loop can suspend and resume.
Simple example: Nabil's collect function, suspended during the sleep.
What it is not: a plain function that blocks until the end.
Reflex: who awaits this coroutine?
- I/O-bound vs CPU-bound
- Waiting (network/disk) → asyncio; heavy computing → multiprocessing.
Simple example: Nabil picks asyncio for his APIs, not for heavy math.
What it is not: async to speed up pure computation.
Reflex: is my bottleneck waiting or computing?
In practice — Discovering async/await with asyncio
- Write a “async def task(n)” coroutine that prints a message, waits (await asyncio.sleep(1)) then prints the end.
- Launch it with asyncio.run() and observe the sequential behaviour of a single task.
- Launch several tasks IN PARALLEL with asyncio.gather(): see that 3 waits of 1 s take ~1 s in total, not 3.
- Conclude: async shines when you WAIT (network, I/O), not to speed up heavy computation.
Ready-to-say script — justifying async
“In Algiers, our morning collection awaits each source one after another: the bottleneck is network waiting, not computation. I suggest moving the loop to coroutines with gather and an async client: waits will overlap. I will measure before/after to prove the gain, and keep pure computation out of async.”
Say to your manager to get a targeted, measured rewrite approved without touching computation.
Copy-paste template — generic async collection
import asyncio
async def tache(nom):
print(f"debut {nom}")
await asyncio.sleep(1)
print(f"fin {nom}")
return nom
async def main():
return await asyncio.gather(*(tache(n) for n in ["a", "b", "c"]))
if __name__ == "__main__":
print(asyncio.run(main()))
# In network production: replace asyncio.sleep with httpx/aiohttp (async-compatible).
Solo / tight budget
Nabil alone: gather over his existing sources is enough — no new infra, just coroutines.
Small business / small team
A shared async HTTP client: the whole team collects without multiplying blocking sessions.
SME in Algiers
Nabil measures before/after in Algiers on the real collection: the proven gain justifies the rewrite.
Starting team
Pair up: one converts a source to async, the other hunts down remaining blocking calls.
✓ Best practices
- Async for I/O, multiprocessing for CPU: choose by the bottleneck.
- gather to launch together; async-compatible libraries (httpx).
- Measure before/after: prove the gain instead of assuming it.
✗ Common pitfalls
- Calling requests (blocking) inside async.
- Forgetting an await: a ghost coroutine never executed.
- Putting async on pure computation (no gain, free complexity).
Key takeaways
- Async keeps the program busy during waiting instead of blocking it.
- async defines a coroutine; await pauses while waiting for a result.
- asyncio.gather() launches several waiting tasks in parallel.
- asyncio serves I/O-bound cases (network, disk), not heavy computation (→ multiprocessing).
48h challenge — apply tomorrow
Convert 1 real waiting loop (APIs, files) to gather: do the 3 waits still take 3 times the time?
- I can explain async/await/gather in one sentence.
- My gather demo overlaps waits (measured proof).
- I know why requests is banned inside async.
Frequently asked questions
Async, threads, multiprocessing: what is the difference?
Three answers to three bottlenecks. asyncio handles I/O-wait concurrency in a single thread (ideal for network/disk). Threading also shares one process but suits some blocking I/O. Multiprocessing spawns several processes to parallelise computation across cores. Choose by whether the bottleneck is waiting (asyncio) or computing (multiprocessing).
Does async necessarily make a program faster?
No. It speeds up wait-dominated workloads (many simultaneous network requests). For pure computation (CPU-bound), it brings nothing and may needlessly complicate the code. Use it where there is idle time to exploit — and measure to prove it.
When does asyncio bring a gain?