5.2 · Asynchronous programming (asyncio)

Level 5 · Pro level & industrialization

5.2Asynchronous programming (asyncio)

Goal: Handle concurrency with async/await and asyncio.
Estimated time: 15 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: “Async makes everything faster, and requests works inside it.” No. Async only speeds up wait-dominated (I/O-bound) workloads; on pure computation, it complicates with no gain. And requests, synchronous and blocking, paralyses the loop: in async, use async-compatible libraries (httpx/aiohttp) — otherwise you paid the complexity without touching the gain.
Waiting together Declare, wait without blocking, launch together async def Declarethe coroutineawait Wait withoutblockinggather Launch together
Declare, wait without blocking, launch together
Text version of the diagram (SEO / screen readers / translation): 1) async def: Nabil declares his data collection in Algiers as coroutines. 2) await: each network wait frees the thread instead of blocking it. 3) gather: all collections start together — waits overlap. Lesson: async-compatible libraries end to end, and measured before/after gain.

Did you know?

Forgetting an await creates a ghost coroutine: it exists but never runs — Python raises a quiet warning many people ignore.
In async, forgetting does not crash: nothing happens.

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

  1. Write a “async def task(n)” coroutine that prints a message, waits (await asyncio.sleep(1)) then prints the end.
  2. Launch it with asyncio.run() and observe the sequential behaviour of a single task.
  3. Launch several tasks IN PARALLEL with asyncio.gather(): see that 3 waits of 1 s take ~1 s in total, not 3.
  4. Conclude: async shines when you WAIT (network, I/O), not to speed up heavy computation.
You understand async/await and asyncio, and know when async brings a real gain (I/O waits): a sought-after advanced skill.

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?

Section exit checklist
  • I can explain async/await/gather in one sentence.
  • My gather demo overlaps waits (measured proof).
  • I know why requests is banned inside async.
Self-rating 1-5: did I separate waiting from computing before coding?is any blocking call left in my async chain?

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.

Check your understanding

When does asyncio bring a gain?

Additional resources (optional, to go further)

Level 5 examFresh, harder questions to test what you've learned