Async Programming with asyncio

Python 3.14 ยท โœ“ verified by execution on 2026-07-30

Asyncio is a library to write concurrent code using the async/await syntax. It is the foundation of multiple Python asynchronous frameworks that provide high-performance network and web-servers, database connection libraries, distributed task queues, etc.

Coroutines and the Event Loop

A coroutine is a function you define with async def. Calling it does not run it โ€” it hands you an object that only makes progress when something awaits it. That is the whole trick behind asyncio: your code decides exactly where it is willing to pause.

asyncio.run() is the doorway from ordinary synchronous Python into that world. It starts an event loop, runs your top-level coroutine to completion, and shuts the loop down again. You call it once, at the entry point โ€” never inside another coroutine.

Basic coroutine execution

python
import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(0)
    print("Async World!")

asyncio.run(say_hello())
Output
Hello
Async World!

Awaiting Coroutines

You can await other coroutines inside an async function to get their return values.

Awaiting another coroutine

python
import asyncio

async def get_greeting():
    return "Hello from coroutine"

async def main():
    greeting = await get_greeting()
    print(greeting)

asyncio.run(main())
Output
Hello from coroutine

Tasks and Cooperative Scheduling

Awaiting a coroutine directly runs it to completion before moving on. Wrapping it in a Task schedules it instead, so the loop can start it and immediately go do something else. Tasks are how you get concurrency rather than just asynchronous-looking sequential code.

The scheduling is cooperative, and that word is doing a lot of work. One task runs at a time; the loop can only switch when a task hits an await and voluntarily yields. A task that never awaits โ€” a long calculation, say โ€” blocks everything else, because nothing can interrupt it.

Running tasks concurrently with gather

python
import asyncio

async def worker(name, delay):
    print(f"{name} started")
    await asyncio.sleep(delay)
    print(f"{name} finished")

async def main():
    await asyncio.gather(
        worker("Task A", 0.02),
        worker("Task B", 0.01)
    )

asyncio.run(main())
Output
Task A started
Task B started
Task B finished
Task A finished

Gather preserves order

python
import asyncio

async def fetch_data(id, delay):
    await asyncio.sleep(delay)
    return f"Data {id}"

async def main():
    results = await asyncio.gather(
        fetch_data(1, 0.02),
        fetch_data(2, 0.01)
    )
    print(results)

asyncio.run(main())
Output
['Data 1', 'Data 2']

Modern Concurrency: TaskGroups

asyncio.TaskGroup (Python 3.11+) is the modern way to run several tasks together. It waits for all of them, and if any one fails it cancels the rest and propagates the error โ€” no silently-abandoned tasks, which was easy to end up with using bare gather().

Using a TaskGroup context manager

python
import asyncio

async def worker(name):
    print(f"Working on {name}")

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(worker("A"))
        tg.create_task(worker("B"))
    print("All tasks completed")

asyncio.run(main())
Output
Working on A
Working on B
All tasks completed

Concurrent execution visualization

You can spawn hundreds of tasks instantly. The total time depends only on the longest individual delay, because all waiting happens concurrently!

python ยท visualize
import asyncio

async def process_item(item_id):
    await asyncio.sleep(0.01 * (5 - item_id))
    print(f"Processed item {item_id}")

async def main():
    tasks = 3
    print(f"Starting {tasks} tasks")
    async with asyncio.TaskGroup() as tg:
        for i in range(1, tasks + 1):
            tg.create_task(process_item(i))
    print("All done")

asyncio.run(main())
Starting 3 tasks
Processed item 3
Processed item 2
Processed item 1
All done

Check yourself

How does asyncio achieve concurrency in Python?

Reveal answer

By using cooperative multitasking on a single thread. โ€” asyncio is single-threaded and uses cooperative multitasking, meaning it achieves concurrency but not true parallelism.

How should you pause a coroutine to wait for a specific duration?

Reveal answer

Use await asyncio.sleep() โ€” Using blocking functions like time.sleep() will block the entire event loop, stopping all other tasks. You must use await asyncio.sleep().

What happens when you call a function defined with `async def` without using `await`?

Reveal answer

It returns a coroutine object but does not execute its body. โ€” Calling a coroutine function only returns a coroutine object. It doesn't run until you await it or schedule it in an event loop.

Challenges

Challenge 1 +100 XP

Write an async function countdown(start) that prints the starting number, awaits for 0.01 seconds, and then prints 'Go!'. Then create a main coroutine to run it with a start value of 3.

python
  • Test 1 โ€” expects "3\nGo!\n"
Need a hint? (โˆ’25% XP)

Use await asyncio.sleep(0.01) to pause the coroutine without blocking.

Show solution (0 XP)
import asyncio

async def countdown(start):
    print(start)
    await asyncio.sleep(0.01)
    print('Go!')

async def main():
    await countdown(3)

asyncio.run(main())

Challenge 2 +100 XP

Use asyncio.TaskGroup to run three tasks concurrently. Create an async function ping(name) that prints f'Ping {name}'. In main(), use a TaskGroup to spawn three ping tasks for 'A', 'B', and 'C'.

python
  • Test 1 โ€” expects "Ping A\nPing B\nPing C\n"
Need a hint? (โˆ’25% XP)

Use the async with asyncio.TaskGroup() as tg: pattern and tg.create_task().

Show solution (0 XP)
import asyncio

async def ping(name):
    print(f'Ping {name}')

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(ping('A'))
        tg.create_task(ping('B'))
        tg.create_task(ping('C'))

asyncio.run(main())