Asynchronous Programming
Source: Fluent Python, 2nd Edition — Chapter 22 by Luciano Ramalho (through sequential
awaits in one download task)
Videla & Williams: async is all or nothing. One blocking call stalls the loop; the rest of your async def was theatre.
This chapter: async def / await / async with / async for; the objects that implement them; a thin slice of asyncio (plus aiohttp in the book). Builds on generators (17), with (18), classic coroutines (19). Also: don’t block the loop (run_in_executor), and “How async works and how it doesn’t” later. Hattingh, Using Asyncio in Python. After Selivanov, the docs split app APIs from framework guts. asyncio.Future is that low-level pile now.
Three coroutine kinds (3.5+)
Native: async def. Always a native coroutine, even with no await. await only inside native coroutines. Delegate with await (like yield from for classic).
Classic: generator + .send / yield. Not driven by await. asyncio dropped them.
Generator-based: @types.coroutine so a generator is awaitable. Still used in Curio/Trio guts. @asyncio.coroutine deprecated 3.8, gone 3.11.
This lesson is native only.
blogdom.py
Check kw.dev for keywords of length ≤ 4. + means DNS resolved (loop.getaddrinfo); blank means socket.gaierror — might be free. Output order = completion, not keyword order. Wall time ≈ slowest lookup, not the sum.
async def probe(domain: str) -> tuple[str, bool]:
loop = asyncio.get_running_loop() # 3.7+; no running loop → RuntimeError
try: # faster than get_event_loop
await loop.getaddrinfo(domain, None)
except socket.gaierror:
return (domain, False)
return (domain, True)
async def main() -> None:
...
coros = [probe(d) for d in domains] # objects, not yet run
for coro in asyncio.as_completed(coros):
domain, found = await coro # done; await still needed for the value
...
asyncio.run(main())
Guido’s squint: ignore async/await and read it as sequential — except await suspends this probe('if.dev'), the loop drives probe('or.dev'), then resumes when DNS answers.
as_completed here is the asyncio cousin of futures.as_completed.
Awaitable
for wants iterables; await wants awaitables. Daily:
- Native coroutine object (
probe(d)). asyncio.Taskfromcreate_task. Schedule without waiting (spinner).await other()when you need the result now.
Lower: __await__ → iterator (Task subclasses Future); C am_await. PEP 492: await is yield from plus “is this awaitable?”
flags with aiohttp
3.9 stdlib: TCP/UDP, no HTTP. Book uses aiohttp. Happy path first.
download_many stays a plain function so flags.main can call it: return asyncio.run(supervisor(cc_list)).
async def supervisor(cc_list):
async with ClientSession() as session: # all aiohttp I/O lives here
to_do = [download_one(session, cc) for cc in sorted(cc_list)]
res = await asyncio.gather(*to_do) # wait for all; list in *submit* order
return len(res)
download_one awaits get_flag; get_flag does async with session.get(url) as resp: return await resp.read(). You cannot reuse requests.get — it blocks the loop. You cannot reuse the threadpool download_one either: it doesn’t await. All-or-nothing.
save_flag is sync disk I/O. Node people treat that as blocking; CPython often shrugs. Fix: run_in_executor (below).
Humble generators
You don’t .send. The loop sends. Your awaits form a channel: user coro → aiohttp → low-level generator the loop ticks on sockets/timers (often C). gather / create_task = several such channels, one thread.
async with
Classic with is __enter__ / __exit__ — not coroutines. DB transactions (asyncpg) need await tr.start() / commit / rollback without stalling everyone. PEP 492: __aenter__ / __aexit__ as coroutines. async with connection.transaction(): .... Same for ClientSession / ClientResponse. (contextlib async generators: later, after async gen functions.)
Native coroutines
1 / 2asyncio.run(main()) where main is async def
Pick an answer to see if you are right.
sorted on ' by' vs '+ax': space (ASCII 32) before + (43), so ' by +ax'. Starter never probes.
flags2 asyncio: semaphore + as_completed
Same CLI as Chapter 21. asyncio can open connections faster than threads — more DoS-shaped. Test locally. Default max concurrent in the book’s asyncio client is 5 (REMOTE 503s).
gather is useless for tqdm: it waits for all. Use asyncio.as_completed, wrap with tqdm(..., total=len(cc_list)).
Throttle: threadpool used max_workers. Here: asyncio.Semaphore(concur_req). Counter: acquire (coro, may suspend if 0) / release (never blocks). async with semaphore: around get_flag only — this coro waits; the loop keeps running. __aenter__ awaits acquire; __aexit__ releases. BoundedSemaphore forbids the counter from exceeding the start value.
404 → not_found inside download_one. Other ClientResponseError → raise FetchError(cc) from exc (PEP 3134). You cannot key a dict by the objects as_completed yields: asyncio may replace your awaitables with wrappers that still produce the same result. Carry cc on the exception instead.
download_many builds supervisor(...) and asyncio.runs it — main cannot call a coroutine directly.
run_in_executor
save_flag freezes the whole loop (one thread). Threads: one worker blocks, GIL dropped on I/O. Fix:
loop = asyncio.get_running_loop()
loop.run_in_executor(None, save_flag, image, f'{cc}.gif')
None = the loop’s default thread pool. Pass a ProcessPoolExecutor (created once in supervisor) for CPU Python. Tiny GIFs: no win; 10× bytes or megapixels: yes. asyncio itself wraps blocking socket.getaddrinfo this way. Motor: async façade over threads. Libraries often hide the executor so callers only await.
The flags2 snippet does not await that call — save races with the rest. For “file is on disk before we continue,” await loop.run_in_executor(...).
Two HTTP calls, one task
Flag bytes then metadata.json for the country name → filename. Threads: two blocking GETs, locals in one function. Callbacks: pyramid of doom. await get_flag then await get_country in the same coroutine — JS and Python both grew await for this.
Don’t gather them if a failed flag makes metadata pointless. Two separate async with semaphore: blocks: hold the permit only while that request runs.
Some APIs mix plain methods and coroutines (e.g. StreamWriter later). Rule of thumb: await awaitables; if it isn’t one, don’t.
Servers next.
Semaphore and gather
1 / 2asyncio.gather(*download_ones)
Pick an answer to see if you are right.