Python for JS Devs · Fundamentals

Async Python & the event loop

You already understand the event loop better than most Python devs. Here's how that knowledge maps — and the one place it doesn't.

Lesson 10Time ~14 min Win Run two awaitables concurrentlyPrereq Lesson 09

This lesson is your bridge into FastAPI. Python's async/await is modelled on the same single-threaded, cooperative event loop you know from Node — but with a few explicit steps JS hides.

1 · The syntax is nearly identical

JavaScript
async function getUser(id) {
  const res = await fetch(url);
  return res.json();
}
Python
async def get_user(id: str):
    async with httpx.AsyncClient() as c:
        res = await c.get(url)
        return res.json()

async def defines a coroutine; await suspends it until the awaited thing resolves. Same mental model as JS.

2 · The difference: coroutines are lazy

Key mismatch from JS In JS, calling an async function starts it immediately and returns a Promise that's already running. In Python, calling get_user() returns a coroutine object that has not started. Nothing runs until you await it or hand it to the event loop. A common beginner bug is calling an async function and forgetting to await it — you get a "coroutine was never awaited" warning and no work happens.

3 · You need an event loop to start

In Node the loop is always running. In a plain Python script you must start it with asyncio.run() at the top level. Inside FastAPI you won't do this — FastAPI runs the loop for you — but you need to know it exists.

import asyncio

async def main():
    await get_user("42")

asyncio.run(main())   # starts the loop; the entry point

4 · Concurrency: gather ≈ Promise.all

JavaScript
const [a, b] = await Promise.all([
  getUser("1"),
  getUser("2"),
]);
Python
a, b = await asyncio.gather(
    get_user("1"),
    get_user("2"),
)

Awaiting them one after another is sequential (slow); gather runs them concurrently on the one loop — exactly like Promise.all.

The rule that will save you in FastAPI One event loop, one thread. A blocking call (a synchronous DB query, time.sleep, heavy CPU work) freezes the entire loop — every other request waits. This is the same "don't block the event loop" law you live by in Node. Lesson 17 is entirely about the FastAPI version of this trap. Async libraries (httpx.AsyncClient, async DB drivers) exist precisely to avoid it.

🎯 Your win

Write a script with async def slow(n): await asyncio.sleep(1); return n. First await two calls sequentially (feel the ~2s), then run them with asyncio.gather (feel the ~1s). Seeing concurrency collapse the time — and knowing it's the same loop you know from Node — is the win.

Check yourself

Q1. Calling a Python async def function returns:
Unlike JS, it returns an un-started coroutine; nothing runs until you await it.
Q2. The equivalent of Promise.all is:
asyncio.gather(...) runs awaitables concurrently on one loop.
Q3. A blocking synchronous call inside async code will:
One loop, one thread — blocking work stalls every other task. This is the core FastAPI gotcha (Lesson 17).
Primary source Python docs — Coroutines and Tasks. Read the "Coroutines" and "Running Tasks Concurrently" sections.
💬 Ask your teacher: what's a Task vs a coroutine, and what does asyncio.create_task do? Perfect question before FastAPI.