await suspends your function until something else is ready. That is the core idea, but it takes time to internalize what suspension really means — your function pauses, the event loop runs other work, and control comes back to you when the awaited result is ready.
Think of await like ordering coffee at a busy café. You place your order (start the operation), step aside from the counter (suspend), and the barista calls your name when it is ready (resume). While you wait, other customers can order and receive their drinks. That is cooperative concurrency: you yield the counter willingly, trusting that you will be served when your drink is ready.
The async keywords give Python a native way to express cooperative suspension.
async defdefines a native coroutine function- calling it creates a coroutine object
awaitsuspends that coroutine until another awaitable produces a result
The right mental model for async is cooperative suspension:
- your coroutine runs
- it reaches
await - it suspends
- the event loop can run something else
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+import asyncioasync def fetch_flag(): await asyncio.sleep(0.1) return "BR"print(asyncio.run(fetch_flag()))Coroutine function ───────────────────────────────
- declared with
async def
Coroutine object ─────────────────────────────────
- created when a coroutine function is called
Awaitable ────────────────────────────────────────
- an object that can appear to the right of
await
Event loop ───────────────────────────────────────
- the scheduler that drives tasks and other awaitables forward when they become ready
The official docs use "awaitable" as the umbrella term because not everything awaited is the same kind of object. Coroutines, tasks, and futures all fit under that umbrella.
One of the easiest ways to read async code is to ignore the scheduling details first and read it almost as if it were synchronous, then come back and mark every await as a potential suspension boundary.
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+async def load_user_page(client, user_id): profile = await fetch_profile(client, user_id) orders = await fetch_orders(client, user_id) return profile, ordersFirst reading:
- load profile
- load orders
- return both
Second reading:
- the coroutine may suspend at
await fetch_profile(...) - then suspend again at
await fetch_orders(...)
That second pass is where latency, concurrency, and failure behavior become visible.
The language reference defines await in terms of the awaitable protocol. In practice, the main awaitables you touch in application code are:
- coroutine objects
asyncio.Taskasyncio.Future
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+import asyncioasync def child(): await asyncio.sleep(0.1) return "ok"async def main(): coro = child() task = asyncio.create_task(child()) return await coro, await taskprint(asyncio.run(main()))The important design fact is that await is about protocol, not spelling. If an object supports the awaitable protocol, Python can suspend on it.
Native coroutines look like a completely new feature, but the mechanism did not appear from nowhere. await inherits much of its coroutine-driving behavior from generator machinery.
That helps explain the implementation lineage, but modern async code is more than "just generators." The key ideas are:
- suspension and resumption
- the event loop that drives the process
The official language-level contract is the async/await behavior itself. The exact internal steps, bytecode, and event-loop plumbing are interpreter and library implementation detail.
A critical production constraint: asynchronous design is not a small cosmetic switch. If one critical layer blocks, the benefit collapses.
# [CURRENT - 3.10-3.14] Works on Python 3.ximport asyncioimport timeasync def bad_worker(): time.sleep(0.5) return "done"That coroutine is syntactically async, but it is operationally blocking. It does not cooperate with the event loop while time.sleep is running.
This is why async systems need boundary discipline:
- async-compatible network/database clients
- explicit delegation for blocking calls
- careful CPU-bound escape hatches
See and .
Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.
Important version markers:
async/awaitsyntax: Python 3.5+ PEP 492asyncio.run: Python 3.7+
Because this project targets current Python, native coroutine syntax is the baseline. Generator-based coroutine style is historical context, not a recommended production style.
Calling an async def function does not run it immediately. It creates a coroutine object.
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+async def hello(): return "hi"result = hello()print(result)result.close()Another common mistake is treating await as if it always means "run in parallel." It does not. It means "suspend here until this awaitable completes."
A coroutine that does CPU work or blocking I/O without awaiting is worse than synchronous code hidden behind async syntax, because readers expect event-loop cooperation that never happens.
Use this foundation model when reading or designing async code:
- mark every
await - ask what object is being awaited
- ask whether that object eventually reaches non-blocking external readiness
- ask what happens if it fails, stalls, or never yields
For production lifecycle, cancellation, and bounded fan-out, see .