Python Async/Await — Core Concepts
async/await is Python’s model for cooperative concurrency. It helps one process handle many I/O-bound tasks by switching work whenever a task hits a wait point.
Why Async Exists
A lot of application time is spent waiting:
- HTTP requests
- database responses
- disk/network I/O
- queue operations
If code waits in a blocking style, each worker sits idle during that wait. To scale, you add more threads or processes, which raises memory use and coordination complexity.
Async offers a different tradeoff: one thread, one event loop, many coroutines that voluntarily yield control during waits.
Core Building Blocks
async def creates a coroutine function
async def fetch_user(user_id):
...
Calling it returns a coroutine object. Nothing runs until it is awaited or scheduled.
await pauses at an awaitable
user = await fetch_user(42)
At await, control returns to the event loop, which can run other ready tasks.
Event loop
The event loop is the scheduler. It tracks pending tasks, wakes them when I/O completes, and advances each coroutine to its next await.
Running Coroutines
The modern top-level entry is asyncio.run.
import asyncio
async def main():
...
asyncio.run(main())
Inside main, launch concurrent work with asyncio.gather.
results = await asyncio.gather(
fetch_user(1),
fetch_user(2),
fetch_user(3),
)
gather runs them concurrently and returns results in call order.
Practical Example: Many API Calls
import asyncio
import httpx
async def fetch_json(client, url):
r = await client.get(url, timeout=10)
r.raise_for_status()
return r.json()
async def main(urls):
async with httpx.AsyncClient() as client:
tasks = [fetch_json(client, u) for u in urls]
return await asyncio.gather(*tasks)
If each request takes ~300 ms and you need 100 of them, sequential code can take ~30 seconds. Async can bring wall-clock time much closer to the slowest group of outstanding requests, limited by remote systems and your own concurrency cap.
Concurrency Control
Unbounded concurrency can overload your service, hit rate limits, or exhaust sockets. Use a semaphore.
sem = asyncio.Semaphore(20)
async def bounded_fetch(client, url):
async with sem:
return await fetch_json(client, url)
Now only 20 requests run at once.
Timeouts, Cancellation, and Cleanup
Reliable async code treats cancellation as normal, not exceptional chaos.
- use deadlines/timeouts (
asyncio.timeoutin modern Python) - handle
asyncio.CancelledErrorfor cleanup - close network clients in
async with
import asyncio
async def worker():
try:
await asyncio.sleep(60)
except asyncio.CancelledError:
# release resources, flush metrics, etc.
raise
Common Misconception
Misconception: async makes Python CPU work faster.
Reality: async improves throughput for I/O-bound workloads. CPU-bound tasks still compete under the GIL in one process. For heavy CPU jobs, use multiprocessing or native extensions.
When to Choose Async
Great fit:
- API gateways and microservices
- chat/websocket servers
- crawlers and scrapers
- message consumers with network-heavy steps
Less ideal:
- short scripts with tiny I/O
- CPU-heavy image/video/data crunching
- teams without appetite for async-aware libraries and debugging patterns
Relationship to Other Python Topics
If you already know Python Generators and Python Iterators, async coroutines feel familiar: both involve pause/resume behavior. The difference is that coroutines are orchestrated by an event loop and are designed for awaitable I/O.
One Thing to Remember
Async/await is a scheduling model for I/O waits: coroutines pause at
await, the event loop runs other tasks, and your app keeps progressing instead of idling.
See Also
- Python Basics Python is the programming language that reads like plain English — here's why millions of beginners (and experts) choose it first.
- Python Booleans Make Booleans click with one clear analogy you can reuse whenever Python feels confusing.
- Python Break Continue Make Break Continue click with one clear analogy you can reuse whenever Python feels confusing.
- Python Closures See how Python functions can remember private information, even after the outer function has already finished.
- Python Comprehensions See how Python lets you build new lists, sets, and mini lookups in one clean line instead of messy loops.