Python asyncio
asyncio is Python's standard library for writing concurrent code with async and await. A single thread runs an event loop that juggles many coroutines. Whenever one is waiting on I/O (a network response, a database query, a timer), the loop switches to another that's ready. One process can handle thousands of simultaneous connections without thousands of threads.
asyncio is the foundation under FastAPI, aiohttp, httpx's async client, and most modern Python network services. It's also easy to misuse: one blocking call inside a coroutine stalls everything, and async adds nothing for CPU-bound work.
TL;DR
async defdefines a coroutine function; calling it returns a coroutine object that does nothing until awaited or scheduled.awaitsuspends the current coroutine until the awaited thing completes, letting the loop run others.asyncio.run(main())starts the event loop;asyncio.TaskGroup(3.11+) runs coroutines concurrently with structured error handling.- Use
asyncio.timeout()for deadlines; cancellation is delivered asCancelledError. - Never block the loop: no
time.sleep,requests, or sync DB drivers in coroutines. Use async libraries orasyncio.to_thread. - asyncio helps I/O-bound concurrency. For CPU-bound work, use processes (or free-threaded Python builds).
Quick Example
Fetch several URLs concurrently with a timeout:
All three requests are in flight at once, so total time is roughly that of the slowest request, not the sum. If any request fails, the TaskGroup cancels the others and raises.
Core Concepts
Coroutines and the Event Loop
A coroutine is a function that can pause at await points. The event loop keeps a queue of ready work. When a coroutine awaits something that isn't ready (a socket read, a sleep), it yields control, the loop runs something else, and it resumes the coroutine when the result arrives. This is cooperative multitasking: switches happen only at await, never in the middle of ordinary code.
Tasks: Running Things Concurrently
await coro() runs coroutines sequentially. To run them concurrently, wrap them in tasks, which the loop schedules independently:
Prefer TaskGroup in new code. It guarantees no task outlives the block, which prevents leaked background work and swallowed exceptions.
Timeouts and Cancellation
Cancellation raises asyncio.CancelledError at the task's current await. Clean up in finally or async with blocks, and re-raise CancelledError if you catch it. Swallowing it breaks timeouts and shutdown.
Async Context Managers and Iterators
async with (for connections, sessions, locks) and async for (for streams, paginated APIs, async generators) are how async libraries expose resources that need awaiting to open, close, or produce items.
Synchronization and Limiting Concurrency
asyncio provides Lock, Event, Condition, Semaphore, and Queue. A Semaphore is the standard way to cap concurrency, for example to avoid opening 10,000 connections at once:
Mixing Sync and Async
- Blocking code from async:
await asyncio.to_thread(blocking_fn, arg)runs it in a thread pool so the loop keeps going. Use it for sync libraries without async versions. - CPU-heavy work:
loop.run_in_executor(ProcessPoolExecutor(), fn, arg)moves it to another process. - Async from sync code:
asyncio.run(coro())at program entry points. Don't call it from inside a running loop.
Best Practices
Use Async Libraries End to End
The whole call chain must be async to benefit: httpx/aiohttp instead of requests, asyncpg/psycopg async or SQLAlchemy's asyncio extension instead of sync drivers, aiofiles or threads for file I/O. One sync call in a hot path serializes everything.
Prefer Structured Concurrency
Use TaskGroup (or gather) so every task you start has an owner that awaits it. Bare create_task without keeping a reference can be garbage-collected mid-flight, and its exceptions are only logged as "Task exception was never retrieved".
Put Deadlines on Network Calls
Every external call should have a timeout, either from the client library or from asyncio.timeout. A single hung connection otherwise holds its coroutine, and anything waiting on it, forever.
Bound Concurrency
Unbounded fan-out (one task per item in a 100,000-item list) exhausts sockets and hammers downstream services. Use a Semaphore or a worker pool pulling from an asyncio.Queue.
Common Mistakes
Blocking the Event Loop
Run with PYTHONASYNCIODEBUG=1 (or asyncio.run(..., debug=True)) to get warnings about slow callbacks that block the loop.
Forgetting to Await
Type checkers like mypy and pyright flag unawaited coroutines; enable those checks.
Awaiting Sequentially When You Meant Concurrency
FAQ
When should I use asyncio instead of threads?
For high-concurrency I/O, such as many network connections, websockets, or fan-out API calls, asyncio scales further with less memory and gives you explicit switch points. Threads are simpler when you're working with blocking libraries and modest concurrency. For CPU-bound work, neither helps under the GIL; use multiprocessing, native extensions, or free-threaded CPython builds.
What's the difference between gather and TaskGroup?
Both run awaitables concurrently. TaskGroup (3.11+) is structured: if one task fails it cancels the siblings and raises all failures as an ExceptionGroup, and no task can outlive the async with block. gather returns results in order and, by default, propagates the first exception while leaving other tasks running. Prefer TaskGroup for new code.
Does async make my code faster?
Not per request. It makes a single process handle more concurrent waiting. If your service spends most of its time waiting on databases and APIs, async raises throughput and cuts resource use. If it spends most of its time computing, async adds overhead without benefit.
Can I use asyncio in Jupyter?
Yes. Notebooks already run an event loop, so use top-level await main() directly instead of asyncio.run(main()), which raises "cannot be called from a running event loop".
Related Topics
- Python — The language overview
- FastAPI — An async-first web framework built on asyncio
- Concurrency Patterns — Concurrency models across languages
- Python Generators — The mechanism coroutines grew out of
- Async JavaScript — The same model in JavaScript's event loop