asyncio: Concurrency in Python without threads
In short: With asyncio many waiting tasks (network, files) run side by side in a single thread by making room for each other while waiting.
In more detail: Many programs wait most of the time: for a reply from the server, for the disk, for the database. Instead of starting a thread for every request, asyncio uses cooperative multitasking: an event loop runs coroutines, and each gives up control while waiting (await) so that another can continue.
In Depth
Basic terms
- Coroutine: a function with
async defthat can pause. - await: pauses the coroutine until the result is ready and lets the event loop do other work.
- Task: a scheduled coroutine that runs concurrently (
asyncio.create_task). - Event loop: loop that manages and resumes tasks (
asyncio.run).
Example
import asyncio
async def fetch(name, seconds):
await asyncio.sleep(seconds) # simulates waiting for the network
return f"{name} done"
async def main():
results = await asyncio.gather(
fetch("A", 2), fetch("B", 1), fetch("C", 3)
)
print(results) # after about 3 s, not 6 s
asyncio.run(main())The three tasks wait at the same time. The total time equals the longest, not the sum.
When it helps and when not
Helpful for I/O-bound tasks: many HTTP requests, database accesses, websockets, web servers (FastAPI, aiohttp). For compute-intensive tasks it achieves nothing, since only one thread computes; for those use multiprocessing or concurrent.futures.ProcessPoolExecutor. Blocking calls such as time.sleep() or requests.get() inside a coroutine block the whole event loop; use asyncio.sleep and asynchronous libraries (httpx, aiohttp) or asyncio.to_thread.
Pitfalls
A forgotten await only creates a coroutine object but no execution. Errors in tasks that are not awaited easily get lost; use TaskGroup (Python 3.11+) for clean error handling. End long tasks with timeouts (asyncio.timeout). The same principle (async/await) you know from JavaScript, C# and Rust.
See also: async/await, Event loop, Concurrency