Definition
A program that queries a database or downloads a page spends most of its time waiting for an answer coming from somewhere else. The processor stays free, the program stays stuck on its line, and a hundred requests in a row are enough to make the application slow.
await exists to reclaim that dead time. It suspends the running coroutine until the result arrives and hands control back to the event loop, which uses the pause to move another task forward. One coroutine's waiting becomes another one's working time.
Two conditions: the word can only be used inside a function declared with async, and it sits in front of an object able to be waited on.
import asyncio
async def load_profile(user_id):
# The coroutine stops here, the loop serves other tasks
data = await network_request(user_id)
return data["name"]
# Opens the event loop and starts the coroutine
asyncio.run(load_profile(42))The middle line reads like an ordinary call, and that is the intent: the code keeps the sequential shape of a classic program while execution serves other tasks.
The last line clears up a frequent surprise: a coroutine does not start on its own, and asyncio.run() opens the loop that hosts it.
What await accepts in front of it
Can anything go after that word? No. The object has to be awaitable, exposing the suspension machinery the event loop asks for. You will only meet three of them, with what each one returns once the wait is over.
| Object | Where it comes from | What await gets from it |
|---|---|---|
| Coroutine | Calling an async def function | Its return value |
| Task | asyncio.create_task() | The result, once the task is finished |
| Future | A lower layer, rarely written by hand | The value set by the producer |
The Task stands out on one point: it starts the work as soon as it is created, whereas a coroutine sits still until something waits on it.
An ordinary generator, a list or an integer belongs to none of those families, and the attempt raises a TypeError: the function being called is in fact synchronous, so its result is too.
Waiting is not blocking
The whole distinction comes down to one question: who stops? An ordinary wait freezes the entire program, whereas a wait with await only suspends the coroutine holding it.
The thing being waited on still has to play along. A blocking function called inside a coroutine freezes the asyncio event loop, which needs that call to return before it can hand work out again. The two functions below look alike and have nothing in common.
import time
async def wrong():
time.sleep(2) # freezes the whole program
async def right():
await asyncio.sleep(2) # lets the other tasks runThe gain often disappoints on the first try, because it only shows up if several waits overlap. A row of await lines runs in order and costs the sum of the durations. To make them overlap, the jobs have to be started together.
# Four seconds: the second wait starts after the first one
a = await download(url_1)
b = await download(url_2)
# Two seconds: both downloads leave at the same time
a, b = await asyncio.gather(download(url_1), download(url_2))None of this is parallelism: await creates neither a thread nor a process. Your coroutines take turns on a single thread, each one picking up control when the previous one starts waiting.
Two derived forms
Two control structures carry the same wait without you having to write it.
async with waits for the opening and then the closing of an asynchronous context manager, for instance a network connection or a database session.
async for waits for each item delivered by an asynchronous iterator, the lines of a response arriving one at a time for example, and hands control back between two items.
Same rule for both: inside an asynchronous function, and on an object designed for it, never its classic equivalent.
The mistakes that keep coming back
The first is forgetting. Calling a coroutine without await does not run it: the object it builds is consumed by nobody, and Python reports it at the end with a coroutine was never awaited warning. Nothing crashed, nothing was done either, and that silence makes the case slow to spot.
The second is using it out of context. An await written inside an ordinary function raises a SyntaxError before any execution at all: the compiler has to know from the declaration whether it is dealing with a coroutine. Adding async in front of def fixes the line, but the calling function becomes asynchronous in turn, up to the entry point.
The third one raises no error at all, which is what makes it sneaky: building an asynchronous application on a library that is not. A synchronous network client does not become awaitable just because it is called inside a coroutine. That leaves its asynchronous version, or moving the call aside with asyncio.to_thread(), which brings back the logic of threading.
An asynchronous application sitting on a synchronous client handles its requests one by one, just like the classic version, with the complexity of async on top and no gain. Check that your network libraries have an asynchronous version before choosing this style.
Frequently asked questions
Why does my coroutine never run?
Because the call produced a coroutine object that nothing waits on. It needs an await in front, or asyncio.create_task() when the work should start in the background while the rest carries on.
Can await be written outside an async function?
No, and the refusal comes early: the parser rejects the file before any execution at all. The only exceptions are consoles built for it, such as python -m asyncio or certain notebooks, which wrap every entry inside an event loop.
Does await make a program faster?
Not on its own, and that is a classic disappointment. It reclaims dead time, meaning network, disk or database waits, and brings nothing to a computation that keeps the processor busy from end to end.