Skip to main content

Python Async/Await: Write Non-Blocking Code from Scratch

Expert30 min6 exercises120 XP
0/6 exercises

Imagine you're a chef in a busy kitchen. You don't stand and watch the oven for 20 minutes while your pasta boils on another burner. Instead, you start the oven, move to the pasta, then check the salad -- all without waiting idle. That's exactly what asynchronous programming does for your code.

What Is Asynchronous Programming?

In regular (synchronous) Python, each line runs one after the other. If one line takes 5 seconds -- like downloading a file -- everything else just waits. Asynchronous programming lets your program start a slow task, move on to other work, and come back when the slow task finishes.

Python's async/await syntax, introduced in Python 3.5, gives you a clean way to write non-blocking code. It's built on top of the asyncio library, which provides the event loop that coordinates all the concurrent tasks.


Coroutines: The Building Blocks

A coroutine is a special function declared with async def. When you call it, it doesn't run immediately. Instead, it returns a coroutine object -- a promise that the function will run when you schedule it.

Python
Loading editor...

The await Keyword

The await keyword pauses the current coroutine until the awaited coroutine finishes. You can only use await inside an async def function.

Python
Loading editor...

Running Coroutines with asyncio.run()

asyncio.run() is the main entry point for async programs. It creates an event loop, runs your top-level coroutine, and shuts everything down cleanly when done.

Python
Loading editor...

Concurrent Tasks with asyncio.gather()

The real power of async shows up when you run multiple tasks at once. asyncio.gather() takes multiple coroutines and runs them concurrently. It's like putting three dishes in the oven at the same time instead of cooking them one by one.

Sequential (slow)
# Takes 3 seconds total
async def main():
    a = await fetch_user(1)   # 1 sec
    b = await fetch_user(2)   # 1 sec
    c = await fetch_user(3)   # 1 sec
    return [a, b, c]
Concurrent (fast)
# Takes ~1 second total
async def main():
    a, b, c = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )
    return [a, b, c]

Let's simulate this difference to see the speed-up:

Python
Loading editor...

Async For and Async Context Managers

Python also provides async for to iterate over asynchronous iterators and async with for asynchronous context managers. These are useful when each iteration or resource setup involves I/O.

Python
Loading editor...

The Event Loop: Orchestrating It All

The event loop is the conductor of the async orchestra. It keeps track of all running tasks, decides which one to wake up next, and handles I/O notifications from the operating system. You rarely interact with the loop directly -- asyncio.run() manages it for you.

Python
Loading editor...

Practice Exercises

Define a Coroutine
Write Code

Write an async def function called greet that takes a name parameter and returns the string Hello, <name>! Welcome to async Python. (where <name> is replaced with the argument). Then, outside the function, print the type of the object returned by calling greet('World') (do NOT await it).

Loading editor...
Predict the Coroutine Output
Predict Output

What does the following code print?

import asyncio

async def add(a, b):
    return a + b

result = add(3, 4)
print(type(result).__name__)
Loading editor...
Build an Async Iterator
Write Code

Create a class called Countdown that implements the async iterator protocol. It should:

1. Accept a start value in __init__

2. Implement __aiter__ returning self

3. Implement __anext__ that counts down from start to 1, raising StopAsyncIteration when done

After the class, print the string Countdown ready to confirm your class is defined.

Loading editor...
Fix the Async Bug
Fix the Bug

The code below tries to define an async context manager but has two bugs. Fix them so that the output shows:

Entering context
Doing work
Exiting context

Since we can't run async code directly, the fixed class should print Context manager ready after definition.

Loading editor...
Simulate Concurrent Task Results
Write Code

Write a function called simulate_gather that takes a list of regular (synchronous) functions, calls each one, collects the results into a list, and returns that list. This simulates what asyncio.gather() does. Then call it with three lambda functions: lambda: 'A', lambda: 'B', lambda: 'C' and print the result.

Loading editor...
Refactor to Async Pattern
Refactor

The synchronous code below processes users sequentially. Refactor it to use async def syntax that *would* run concurrently with asyncio.gather(). Keep the same logic but:

1. Make fetch_user an async def

2. Make process_all an async def that uses await on each fetch_user call

3. After the definitions, print Refactored to async to confirm.

Loading editor...