What you'll learn
Async lets a single thread juggle thousands of I/O operations by switching between them whenever one is waiting. No threads, no locks, no race conditions — just cooperative pauses at await.
By the end of this lesson you'll be able to:
- Explain what problem async solves and how the event loop works
- Write coroutines with
async defandawait - Run many coroutines concurrently with
asyncio.gather - Group related tasks safely with
asyncio.TaskGroup - Know when async is the right tool — and when it isn't
What async solves
Threads work for concurrency, but each thread carries memory and switching overhead — a few thousand is a lot. Async runs on one thread and an event loop: whenever a task hits an await on something slow, the loop parks it and runs another ready task. This scales to tens of thousands of simultaneous connections, which is why web servers and network tools love it.
Coroutines: async & await
A function defined with async def is a coroutine. Calling it doesn't run it — it returns a coroutine object you must await (or hand to the event loop with asyncio.run). await means "pause here until this finishes, and let other work run meanwhile."
import asyncio
async def greet(name):
print(f"hello {name}")
await asyncio.sleep(1) # give control back while "waiting"
print(f"bye {name}")
asyncio.run(greet("Ada")) # run the coroutine to completionRunning many at once
The payoff appears when you await many things together. asyncio.gather starts all the coroutines and waits for them concurrently — three one-second calls finish in about one second, not three:
import asyncio, time
async def fetch(n):
await asyncio.sleep(1) # simulate a 1s network call
return n * 10
async def main():
start = time.perf_counter()
results = await asyncio.gather(fetch(1), fetch(2), fetch(3))
print(results, f"in {time.perf_counter() - start:.0f}s")
asyncio.run(main())Key idea
gather returns results in the order you passed them, regardless of which finished first. This is the async equivalent of the thread pool from the last lesson — but on a single thread, with no locks.Structured concurrency
Modern asyncio (Python 3.11+) prefers asyncio.TaskGroup: it starts tasks and guarantees they all complete before the async with block exits. If any task raises, the group cancels the rest and propagates the error — no silently lost tasks.
import asyncio
async def work(n):
await asyncio.sleep(0.1)
return n * n
async def main():
async with asyncio.TaskGroup() as tg: # Python 3.11+
t1 = tg.create_task(work(2))
t2 = tg.create_task(work(3))
# the block exits only when every task is done
print(t1.result(), t2.result())
asyncio.run(main())When to use async
Async shines for high-concurrency I/O: many network calls, web servers, chat, scraping. The catch is that everything must cooperate — one blocking call freezes the entire loop.
import asyncio, time
async def bad():
time.sleep(1) # WRONG: blocks the whole event loop!
async def good():
await asyncio.sleep(1) # RIGHT: yields control while waitingWatch out
time.sleep, requests.get, heavy CPU loops) directly in a coroutine — it blocks every other task. Use async libraries (asyncio.sleep, httpx, aiohttp) or offload blocking work with asyncio.to_thread(...). For CPU-bound work, use processes instead.Recap & quick check
Key takeaways
- Async runs on one thread with an event loop, switching tasks whenever one awaits something slow.
- async def defines a coroutine; calling it returns a coroutine object you must await or asyncio.run.
- await pauses the current coroutine and lets other ready tasks run meanwhile.
- asyncio.gather runs many coroutines concurrently and returns results in input order.
- asyncio.TaskGroup (3.11+) gives structured concurrency: all tasks finish, errors cancel the rest.
- Async is for high-concurrency I/O; a single blocking call freezes the whole loop — use async libraries or offload work.
Quick check
1. What does calling an async def function do?
2. What does await do?
3. Why do three 1-second fetches in asyncio.gather finish in ~1 second?
4. What happens if you call time.sleep(1) inside a coroutine?
5. Which workload is async best suited to?
You can now write fast, concurrent code. Next we make sure it's correct code — and stays that way. Next up: Module 32 — Testing.