Python async and await
async and await let one Python program wait for many slow things at the same time, instead of one after another.
Why Async?
Think of a tea stall. The worker puts water on to boil. A slow worker stands and watches the pot until it boils, and only then takes the next order. A smart worker takes more orders while the water heats.
Programs often wait too: for a web page to download, a database to answer, or a file to arrive. Asynchronous (async) code lets Python start other work during those waits.
| Word | Meaning |
|---|---|
async def | Defines a coroutine: a function that can pause |
await | "Pause here until this finishes, and let other work run meanwhile" |
asyncio | The built-in module that runs coroutines |
| Event loop | The manager that switches between waiting coroutines |
| Task | A coroutine that has been scheduled to run |
Your First Coroutine
Example
import asyncio
async def greet(name):
print(f"Hello {name}...")
await asyncio.sleep(1)
print(f"...goodbye {name}")
asyncio.run(greet("Ayesha"))Hello Ayesha... ...goodbye Ayesha
async defmakesgreeta coroutine.await asyncio.sleep(1)pauses for one second without blocking the program.asyncio.run()starts the event loop and runs the coroutine. You call it once, at the top of your program.
Calling greet("Ayesha") on its own does not run the code. It only creates a coroutine object. You must await it or pass it to asyncio.run().
One After Another
First, the slow way. Each order waits for the one before it.
Example
import asyncio
import time
async def make(item, seconds):
await asyncio.sleep(seconds)
return f"{item} ready"
async def main():
start = time.perf_counter()
a = await make("Tea", 2)
b = await make("Paratha", 3)
c = await make("Omelette", 1)
print(a, b, c, sep=" | ")
print(f"Took {time.perf_counter() - start:.0f} seconds")
asyncio.run(main())Tea ready | Paratha ready | Omelette ready Took 6 seconds
2 + 3 + 1 = 6 seconds. We used async, but we still waited for each item in turn.
At the Same Time With gather()
asyncio.gather() runs several coroutines together and returns all their results, in the same order you passed them.
Example
import asyncio
import time
async def make(item, seconds):
await asyncio.sleep(seconds)
print(f" {item} done after {seconds}s")
return f"{item} ready"
async def main():
start = time.perf_counter()
results = await asyncio.gather(
make("Tea", 2),
make("Paratha", 3),
make("Omelette", 1),
)
print(results)
print(f"Took {time.perf_counter() - start:.0f} seconds")
asyncio.run(main())Omelette done after 1s Tea done after 2s Paratha done after 3s ['Tea ready', 'Paratha ready', 'Omelette ready'] Took 3 seconds
Now the whole order takes only as long as the slowest item: 3 seconds instead of 6. The omelette finished first, but results still keeps the original order.
Tasks With TaskGroup
From Python 3.11, asyncio.TaskGroup is the modern, safer way to start many tasks. If one task fails, the others are cancelled cleanly.
Example
import asyncio
async def download(page, seconds):
await asyncio.sleep(seconds)
return f"{page} ({seconds}s)"
async def main():
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(download("home", 0.3))
t2 = tg.create_task(download("courses", 0.1))
t3 = tg.create_task(download("contact", 0.2))
print(t1.result(), t2.result(), t3.result(), sep=", ")
asyncio.run(main())home (0.3s), courses (0.1s), contact (0.2s)
When the async with block ends, every task in the group has finished.
Timeouts
Do not wait forever for something slow. asyncio.timeout() (Python 3.11+) stops the work after a limit.
Example
import asyncio
async def slow_server():
await asyncio.sleep(5)
return "data"
async def main():
try:
async with asyncio.timeout(1):
print(await slow_server())
except TimeoutError:
print("Server too slow, showing saved data instead")
asyncio.run(main())Server too slow, showing saved data instead
A Common Mistake: Blocking Code
Inside a coroutine, never use a normal blocking call like time.sleep() or requests.get(). It freezes the whole event loop, so nothing else can run.
Example
import asyncio
import time
async def bad(n):
time.sleep(1)
return n
async def main():
start = time.perf_counter()
await asyncio.gather(bad(1), bad(2), bad(3))
print(f"Took {time.perf_counter() - start:.0f} seconds")
asyncio.run(main())Took 3 seconds
Three tasks ran one by one because time.sleep() does not let go. With await asyncio.sleep(1) it would take 1 second. For web requests, use an async library such as httpx or aiohttp instead of requests.
When Should You Use Async?
| Good fit | Not a good fit |
|---|---|
| Calling many APIs or web pages | Heavy maths or image processing |
| Chat servers and bots | Simple scripts that do one thing |
| Web frameworks like FastAPI | Code that only reads a few local files |
Async speeds up waiting, not thinking. If your program is slow because the CPU is busy calculating, async will not help. Look at the multiprocessing module for that.
Exercise
Exercise
Three coroutines sleep for 1, 2 and 4 seconds. About how long does asyncio.gather() take to run all three?
