3.3.1 async & await
Ordinary Python waits for each slow call to finish before it starts the next one. That is fine for CPU work. It wastes time when the program is mostly waiting — network responses, disk, timers.
asyncio lets one thread pause a task at an await and run another task that is ready. This is cooperative concurrency, not multiple CPU cores.
Coroutines
A function defined with async def is a coroutine function. Calling it does not run the body; it returns a coroutine object. Something must await it, usually the event loop.
import asyncio
async def fetch_title(url: str) -> str:
await asyncio.sleep(0.2) # stand-in for a network call
return url.rsplit("/", 1)[-1]
async def main() -> None:
titles = await asyncio.gather(
fetch_title("https://example.com/a"),
fetch_title("https://example.com/b"),
fetch_title("https://example.com/c"),
)
print(titles)
asyncio.run(main())
asyncio.gather starts the three fetches together. Total wait is about 0.2 s, not 0.6 s.
Rules of thumb
- Use
awaitonly insideasync def. - Do not call blocking functions (
time.sleep, ordinaryrequests.get) inside a coroutine. They freeze the whole loop. Useasyncio.sleepand async HTTP clients instead. - CPU-heavy work (big loops, image processing) does not get faster with
async. Use processes or a thread pool for that.
Tasks
asyncio.create_task schedules a coroutine to run in the background of the current loop:
async def main() -> None:
task = asyncio.create_task(fetch_title("https://example.com/x"))
print("started")
title = await task
print(title)
Await the task before the program ends, or the work may be cancelled.
When to stay synchronous
If you write a script that calls one API once, a normal function is clearer. Reach for async when you have many independent I/O waits — scraping many URLs, talking to several services, or a network server that must stay responsive.