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

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

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

References