Q&A library Python

How do async and await work in Python?

Python Advanced async-awaitasyncioconcurrency

Short answer

async def creates a coroutine, await pauses it while waiting so other tasks can run, and asyncio.run starts the event loop. It speeds up code that waits on input and output, not CPU-bound work.

The short version

async def defines a coroutine, await pauses it while it waits for something (a network reply, a timer) and lets other tasks run meanwhile, and asyncio.run starts the event loop that drives them. This makes code that spends its time waiting much faster by overlapping the waiting. It does not speed up CPU-bound work, which is limited by the GIL.

A first coroutine

import asyncio

async def fetch(n):
    await asyncio.sleep(1)   # stand-in for waiting on a network call
    return n * 2

async def main():
    results = await asyncio.gather(fetch(1), fetch(2), fetch(3))
    return results

print(asyncio.run(main()))   # [2, 4, 6], after about 1 second, not 3

The three fetch calls wait at the same time, so the whole thing takes about one second rather than three. asyncio.gather runs them concurrently and collects the results.

What await actually does

await says "pause here until this finishes, and while paused, let the event loop run something else". So while one coroutine waits on a slow reply, others make progress. This is cooperative: a coroutine only yields control at an await, so a coroutine that never awaits blocks everything.

Why it helps only I/O-bound work

The speed-up comes from overlapping waiting, so it helps when the bottleneck is input and output: network requests, database queries, file access. For CPU-bound work (heavy computation), there is nothing to wait for, and the GIL still allows only one thread of Python at a time, so async gives no benefit. Use processes for CPU-bound work and async for I/O-bound work.

The common mistakes

Calling a coroutine without awaiting it does nothing useful; it just creates a coroutine object and often warns "coroutine was never awaited".

fetch(1)            # WRONG: nothing runs, returns a coroutine object
await fetch(1)      # correct, inside an async function

And you cannot await at the top level of a script; you need an event loop, which asyncio.run(main()) provides as the entry point.

Running tasks as they are created

asyncio.gather waits for a fixed set. To launch work and manage it, asyncio.create_task schedules a coroutine to run in the background, which you can await later.

task = asyncio.create_task(fetch(1))
# ... do other things ...
result = await task

The async ecosystem

Plain asyncio gives the machinery, but you usually use async libraries built on it: httpx or aiohttp for HTTP, async database drivers, and web frameworks like FastAPI. A key rule is not to mix a blocking call (like ordinary requests or time.sleep) into async code, because it stalls the whole event loop. Use the async equivalent so the loop can keep running other tasks.

Is it worth it

Async adds real complexity, so reach for it when you have many concurrent I/O operations to overlap, such as calling an API hundreds of times. For a handful of calls, or CPU-bound work, simpler approaches (a thread pool, or just sequential code) are often clearer and enough.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project