async & await (asyncio)
Many programs spend most of their time waiting: for a website to reply, a file to download or a database to answer. A normal Python program waits for each job to finish before starting the next. Asynchronous code lets the program start other work while it waits, so ten slow downloads can overlap instead of running one after another.
Python does this with the keywords async and await and the standard asyncio module. In this lesson you will write coroutines, run several of them at the same time, and add timeouts.
The idea: one cook, many pots
Picture a single cook at a dhaba in Lucknow. She puts dal on the stove, and while it simmers she kneads dough, then flips a roti, then checks the dal again. She never stands still waiting for one pot. That is concurrency: one worker switching between tasks whenever a task has to wait.
asyncio works the same way. It runs one task at a time, but when a task reaches an await on something slow, asyncio switches to another task that is ready. This helps with I/O-bound work (waiting for networks, files, databases). It does not make heavy calculations faster, because the cook still has only two hands.
Coroutines: async def and await
A function defined with async def is a coroutine function. Calling it does not run its body. It returns a coroutine object, a job that is ready to run when asyncio schedules it. You can see this in the browser:
async def make_chai(cups):
return f"{cups} cups of chai ready"
job = make_chai(3)
print(type(job).__name__)
print(job.__name__, "has not run yet")
job.close() # we will not run it here, so close it to avoid a warningcoroutine make_chai has not run yet
No chai was made: the body of make_chai() did not run at all. To actually run a coroutine, you await it from inside another coroutine, or hand the top-level coroutine to asyncio.run(). The browser editor cannot run asyncio.run(), so the remaining examples show their output below the code. Run them on your computer to see them work.
import asyncio
async def make_chai(cups):
print(f"Boiling water for {cups} cups...")
await asyncio.sleep(2) # wait without blocking
return f"{cups} cups of chai ready"
async def main():
result = await make_chai(3)
print(result)
asyncio.run(main())Boiling water for 3 cups... 3 cups of chai ready
The pieces fit together like this:
asyncio.run(main())starts the event loop, the scheduler that runs coroutines, and runsmain()until it finishes. Call it once, at the start of your program.awaitpauses the current coroutine until the awaited job finishes, and lets other tasks run meanwhile. You can only useawaitinside anasync deffunction.asyncio.sleep(2)is the async version oftime.sleep(2). It stands in here for any slow operation, such as a network request.
Running tasks concurrently with gather()
Awaiting coroutines one after another is still sequential. To overlap them, start them together. asyncio.gather() runs several coroutines concurrently and returns their results in the same order you passed them.
import asyncio
import time
async def fetch_result(student, seconds):
print(f"Requesting result for {student}")
await asyncio.sleep(seconds) # pretend the server is slow
return f"{student}: Pass"
async def main():
start = time.perf_counter()
results = await asyncio.gather(
fetch_result("Aarav", 2),
fetch_result("Priya", 3),
fetch_result("Kabir", 1),
)
print(results)
print(f"Took {time.perf_counter() - start:.1f} seconds")
asyncio.run(main())Requesting result for Aarav Requesting result for Priya Requesting result for Kabir ['Aarav: Pass', 'Priya: Pass', 'Kabir: Pass'] Took 3.0 seconds
All three requests start at once, so the total time is the longest wait, 3 seconds, not 2 + 3 + 1 = 6 seconds. (On your computer the time may show as 3.0 or very slightly more.) The results come back in the order you listed them, even though Kabir's finished first.
TaskGroup: the modern, safer way
Since Python 3.11, asyncio.TaskGroup is the recommended way to run a group of tasks. You create tasks inside an async with block, and the block waits until all of them finish. If one task fails, the group cancels the others and reports the errors together.
import asyncio
async def download(file_name, seconds):
await asyncio.sleep(seconds)
print(f"Downloaded {file_name}")
return len(file_name)
async def main():
async with asyncio.TaskGroup() as group:
notes = group.create_task(download("notes.pdf", 2))
syllabus = group.create_task(download("syllabus.pdf", 1))
# the block ends only when both tasks are done
print("Sizes:", notes.result(), syllabus.result())
asyncio.run(main())Downloaded syllabus.pdf Downloaded notes.pdf Sizes: 9 12
create_task() schedules a coroutine to run as soon as possible and returns a task object. After the group finishes, task.result() gives each return value. When tasks in a group raise errors, they arrive bundled in an ExceptionGroup, which you handle with except* (see Errors & Exceptions).
Timeouts
Never wait forever for a slow server. asyncio.timeout() (Python 3.11 and later) cancels the work inside its block if it takes too long and raises TimeoutError.
import asyncio
async def slow_payment_gateway():
await asyncio.sleep(10)
return "Payment confirmed"
async def main():
try:
async with asyncio.timeout(2):
print(await slow_payment_gateway())
except TimeoutError:
print("Gateway too slow, please try again")
asyncio.run(main())Gateway too slow, please try again
The program gives up after 2 seconds instead of hanging for 10.
Common mistakes
- Forgetting
await. Writingmake_chai(3)withoutawaitcreates a coroutine that never runs, and Python warnsRuntimeWarning: coroutine 'make_chai' was never awaited. - Using
time.sleep()inside async code. It blocks every task. Useawait asyncio.sleep(). - Calling
asyncio.run()more than once or from inside a coroutine. Call it once to start the program; inside coroutines, useawait. - Expecting asyncio to speed up calculations. It helps with waiting, not with CPU-heavy work like resizing thousands of images.
Key points
async defdefines a coroutine function; calling it returns a coroutine that has not run yet.awaitruns a coroutine and pauses the current one until it finishes, letting other tasks run.asyncio.run(main())starts the event loop; call it once at the top level.asyncio.gather()andasyncio.TaskGrouprun tasks concurrently; total time is roughly the longest task.asyncio.timeout()stops waiting after a limit and raisesTimeoutError.- asyncio suits I/O-bound work; avoid blocking calls such as
time.sleep()inside coroutines.
Exercise
Write an async program for a food delivery app in Pune. Create a coroutine prepare(dish, minutes) that prints when cooking starts, waits minutes seconds with asyncio.sleep() (seconds stand in for minutes), and returns "<dish> ready". In main(), use a TaskGroup to prepare biryani (3), raita (1) and gulab jamun (2) at the same time, then print all three results. Run it on your computer and check that it takes about 3 seconds.
Show solution
import asyncio
import time
async def prepare(dish, minutes):
print(f"Started {dish}")
await asyncio.sleep(minutes)
return f"{dish} ready"
async def main():
start = time.perf_counter()
async with asyncio.TaskGroup() as kitchen:
tasks = [
kitchen.create_task(prepare("biryani", 3)),
kitchen.create_task(prepare("raita", 1)),
kitchen.create_task(prepare("gulab jamun", 2)),
]
print([task.result() for task in tasks])
print(f"Order ready in {time.perf_counter() - start:.0f} seconds")
asyncio.run(main())Started biryani Started raita Started gulab jamun ['biryani ready', 'raita ready', 'gulab jamun ready'] Order ready in 3 seconds
The three dishes cook concurrently, so the order is ready after the slowest dish, not after 1 + 2 + 3 = 6 seconds.
Quick Quiz
1. What does calling an async def function return?
The body runs only when the coroutine is awaited or scheduled as a task.
2. Three coroutines that wait 1, 2 and 3 seconds run with asyncio.gather(). About how long does it take?
They wait concurrently, so the total is about the longest single wait.
3. Which call should you use to pause inside a coroutine?
time.sleep() blocks the whole event loop; asyncio.sleep() lets other tasks run.
4. What kind of work does asyncio speed up?
asyncio overlaps waiting; it does not add more CPU power.
Learn Python with a mentor
Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
