Asynchrony
async, await, and why a bot needs them
What it is
Asynchrony is a way of not standing idle while the program waits for something. An API request takes half a second, and ordinary code just stands there for all of it. Asynchronous code gets other things done meanwhile.
For a bot this is essential: while one user waits for the weather, the rest
must still get answers. To read the code it is enough to understand two words:
async and await.
What it looks like in code
import asyncio
import time
async def fetch_city(name: str) -> str:
await asyncio.sleep(0.3) # as if it were a network request
return f"{name}: +4"
async def main() -> None:
t = time.perf_counter()
for city in ["Oslo", "Bergen", "Tromsø"]:
print(await fetch_city(city))
print(f"one at a time: {time.perf_counter() - t:.1f} s")
t = time.perf_counter()
results = await asyncio.gather(
*(fetch_city(c) for c in ["Oslo", "Bergen", "Tromsø"])
)
print(f"all at once: {time.perf_counter() - t:.1f} s")
asyncio.run(main())Output:
one at a time: 0.9 s
all at once: 0.3 sThree requests of 0.3 seconds each: in sequence, 0.9; together, 0.3. That is the whole benefit of asynchrony, measured.
Three words
async def — the function is asynchronous. The main consequence: calling
it does not run it; it returns a promise object. To get the result you
need await.
await — "wait for the result, and while we wait, go do other things". It
can only appear inside an async def.
asyncio.run(main()) — the launch. The point where the asynchronous world
begins.
Where it pays and where it doesn't
Asynchrony helps where the program waits: network, disk, database. Where the program computes, there is nothing to gain — the processor is busy and there is nothing to switch to.
| Helps | Doesn't help |
|---|---|
| API requests | multiplying matrices |
| database work | image processing |
| reading and writing files | parsing a large text |
| waiting for messages from Telegram | any heavy computation |
The most common mistake: a forgotten await
data = fetch_city("Oslo") # forgot await
print(data)This prints something like <coroutine object fetch_city at 0x...> plus a
coroutine was never awaited warning. The function never ran at all.
A blocking call in asynchronous code
The second common trouble is an ordinary synchronous library inside an
async def:
async def fetch_city(name: str) -> str:
r = requests.get(url) # blocks everything!
return r.textrequests is synchronous: for the duration of the request the whole program
stops, and the bot stops answering anyone. The asynchronous replacements are
aiohttp, or httpx in async mode. Same with databases: sqlite3 blocks,
aiosqlite does not.
The sign while reading: inside an async def, a call without await to
something that goes to the network, the disk or the database.
What to tell the AI
Are there any blocking calls in this asynchronous code?
Name the lines and what to replace them with.Do these three requests run one after another or together?
If one after another, show me how to do them together.Rewrite this so it doesn't block the event loop.
You may change libraries, but say which ones you are adding.What to look for
A forgotten await. The most common one. Found by coroutine in the
output.
A synchronous library inside an async def. requests, time.sleep, a
plain open on a big file — all of them stop the program entirely.
time.sleep(1) has to become await asyncio.sleep(1).
Sequential awaits where one batch would do. A loop with an await
inside runs the requests one after another. If they are independent, combine
them with asyncio.gather — as in the example, three times faster.
Asynchrony where it isn't needed. If the program never waits for anything,
async only adds complexity. The AI sometimes makes everything asynchronous.
Cheat sheet
async defdoes not run when called — it needsawait.awaitonly works inside anasync def.It pays where the program waits: network, disk, database. Not for computation.
<coroutine object ...>— a forgottenawait.requestsandtime.sleepinside anasync defblock everything.Combine independent requests with
asyncio.gather.