Chapter 9 of 13 · Free, no signup

Support Us

Python Mastery · Chapter 9 · ~22 min · Advanced

Asyncio & the Event Loop Race Track

Why wait 10 seconds for 5 API calls when you can finish all of them in 2 seconds? Master Python's event loop, async / await, and high-concurrency non-blocking architecture.

  • Explain the event loop in plain words
  • Write async / await coroutines
  • Spot blocking calls that freeze everything
  • Run tasks concurrently with gather

1The Waiting Game: Why Async Matters

In typical web development, your CPU spends 99% of its time doing nothing: waiting for database queries, network APIs, or disk reads.

Synchronous code sits and blocks like someone waiting in line. Asynchronous code places an order, steps aside to serve the next customer, and comes back when the food is ready.

Live Race Track: 3 Network I/O Requests

Fetch User (1.5s), Fetch Orders (1.0s), Fetch Notifications (0.8s)

Elapsed Time: 0.00s
Parallel Wait · Target: ~1.50s
🌐 Request 1: fetch_user() (1.5s network lag)0%
📦 Request 2: fetch_orders() (1.0s database query)0%
🔔 Request 3: fetch_notifications() (0.8s microservice)0%
Asyncio doesn't make code compute math faster; it makes waiting for external I/O happen concurrently.

2Coroutines and the 'await' Keyword

When you prefix a function with async def, it becomes a coroutine. When you call it, Python pauses and gives you an unstarted promise. The await keyword is how you hand control over to the event loop.

The Coroutine Trap: Calling an async def without await

In normal Python, calling f() executes code. In async Python, calling f() merely builds a coroutine object!

async def get_price(item):
    await asyncio.sleep(1)
    return 49.99

# In your main function:
price = get_price("laptop")  # ⚠️ No await!
print(price)
Terminal Output:
<coroutine object get_price at 0x10a2b0>
RuntimeWarning: coroutine 'get_price' was never awaited!
Code inside get_price NEVER ran! Python just returned an un-started coroutine object.

What does calling an async def function do on its own?

3The Mental Model: Chefs, Timers, and Cores

Is Asyncio multithreading? No! Asyncio uses a single thread with cooperative multitasking. Compare the three concurrency models below:

The Mental Model: The Chef's Kitchen

How Python handles concurrency vs. parallelism.

👨‍🍳 1 Chef + Timers (I/O Bound Work)

The chef puts pasta into boiling water, sets a 10-minute timer, and doesn't stand there staring at the pot. While the water boils, the chef chops tomatoes, seasons garlic, and kneads dough.

Best for: Web servers (FastAPI), scrapers, database calls, API aggregators. (10,000 connections with low RAM).

4The Async Killer: Blocking Calls

Because the event loop runs on one thread, any synchronous blocking function stops all other coroutines from ticking. Never call time.sleep() or synchronous requests.get() inside async functions!

The #1 Async Bug: Blocking the Event Loop Thread

The event loop runs on a single thread. If you call synchronous blocking functions like time.sleep() or requests.get(), you freeze everyone!

# IDIOMATIC ASYNC:
import asyncio

async def worker():
    # Yields control back to event loop!
    await asyncio.sleep(2)  # ⏩ Other tasks keep running!
Event Loop State: COOPERATIVE ✅

await asyncio.sleep registers a timer callback with the event loop and yields the CPU immediately. 10,000 other tasks can run in those 2 seconds.

Mental model

One chef, many timers

The event loop is one chef. await asyncio.sleep(2) sets a timer and the chef cooks something else. time.sleep(2) makes the chef stand frozen staring at the oven — every other dish waits too. Same with requests.get() inside async def; use httpx.AsyncClient instead.

Inside async def, which call freezes ALL other tasks?

5Concurrent Orchestration: gather and TaskGroup

To fire multiple requests at the exact same time, use asyncio.gather() or Python 3.11's TaskGroup:

# Modern Python 3.11+ Structured Concurrency:
async with asyncio.TaskGroup() as tg:
    task1 = tg.create_task(fetch_user(user_id))
    task2 = tg.create_task(fetch_orders(user_id))
    task3 = tg.create_task(fetch_settings(user_id))

# When the block exits, all 3 tasks have finished cleanly!
print(task1.result(), task2.result(), task3.result())

6Challenge: Asyncio Race Engineer

Test your understanding of the event loop, coroutines, non-blocking I/O, and concurrency pitfalls. Score 5/5 to complete Chapter 9!

Asyncio & the Event Loop Race Track · code lab

Edit the example, run it with Python, or step through the lines.

Python
Line-by-line view
1import asyncio
2
3async def fetch(name, delay):
4 await asyncio.sleep(delay)
5 return f"{name} ready"
6
7results = await asyncio.gather(fetch("A", 0.01), fetch("B", 0.01))
8print(results)

Python output

Run the code to see its output.

1.What happens if you call an 'async def' function without 'await'?

2.What is the primary danger of calling time.sleep() inside an async function?

3.Which function is used to run multiple async coroutines concurrently and wait for all of them to finish?

4.When is Asyncio the ideal architectural choice over Multiprocessing?

5.In modern Python (3.11+), what is the recommended context manager for running concurrent tasks safely with error bubbling?

Run and measure the concurrent API aggregator in the Python Playground (code preloaded).

7Recap

  • Event Loop: A cooperative single-threaded scheduler that juggles I/O tasks while they wait.
  • async def: Returns a coroutine object; it will not execute until awaited or scheduled.
  • await: Pauses the current coroutine and yields execution back to the loop.
  • Never Block: Use asyncio.sleep() and httpx/aiohttp, never time.sleep() or requests.
  • asyncio.gather / TaskGroup: Run multiple coroutines concurrently and bundle results cleanly.

Finish the challenge above to complete the chapter, or

Enjoying this free lesson? Your support helps us build more free animated chapters.

Support Us