async & await (asyncio)

Python · Lesson 46 of 56 6 min read Updated

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:

PythonRun it yourself
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 warning
Output
coroutine
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.

Python
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())
Output
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 runs main() until it finishes. Call it once, at the start of your program.
  • await pauses the current coroutine until the awaited job finishes, and lets other tasks run meanwhile. You can only use await inside an async def function.
  • asyncio.sleep(2) is the async version of time.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.

Python
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())
Output
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.

Python
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())
Output
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.

Python
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())
Output
Gateway too slow, please try again

The program gives up after 2 seconds instead of hanging for 10.

Common mistakes

  • Forgetting await. Writing make_chai(3) without await creates a coroutine that never runs, and Python warns RuntimeWarning: coroutine 'make_chai' was never awaited.
  • Using time.sleep() inside async code. It blocks every task. Use await asyncio.sleep().
  • Calling asyncio.run() more than once or from inside a coroutine. Call it once to start the program; inside coroutines, use await.
  • Expecting asyncio to speed up calculations. It helps with waiting, not with CPU-heavy work like resizing thousands of images.

Key points

  • async def defines a coroutine function; calling it returns a coroutine that has not run yet.
  • await runs 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() and asyncio.TaskGroup run tasks concurrently; total time is roughly the longest task.
  • asyncio.timeout() stops waiting after a limit and raises TimeoutError.
  • 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
Python
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())
Output
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?

2. Three coroutines that wait 1, 2 and 3 seconds run with asyncio.gather(). About how long does it take?

3. Which call should you use to pause inside a coroutine?

4. What kind of work does asyncio speed up?

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