The async def and await keywords are Python’s syntax for writing asynchronous code that reads almost like synchronous code. An async def function defines a coroutine — a function that can be paused and resumed. The await keyword pauses the coroutine until the awaited operation completes, yielding control back to the event loop so it can run other coroutines in the meantime. These two keywords are the entire public API for writing async Python — everything else (tasks, queues, semaphores) builds on top of them.
Coroutines — async def and await
import asyncio
# ── Regular function vs coroutine function ────────────────────────────────────
def regular():
return "sync result"
async def coroutine():
return "async result"
# Calling a regular function → executes immediately, returns the value
result = regular() # "sync result"
# Calling a coroutine function → returns a COROUTINE OBJECT (not the result yet)
coro = coroutine() # <coroutine object coroutine at 0x...>
print(type(coro)) # <class 'coroutine'>
# To actually run the coroutine, you need the event loop:
result = asyncio.run(coroutine()) # "async result"
# ── await suspends until the awaited coroutine completes ─────────────────────
async def fetch_data(delay: float, value: str) -> str:
await asyncio.sleep(delay) # non-blocking wait — yields to event loop
return value
async def main():
result = await fetch_data(0.5, "hello") # wait for it to complete
print(result) # "hello"
asyncio.run(main())
async def) and a coroutine object (created by calling that function) are different things. async def greet(): return "hi" defines the function. greet() creates a coroutine object — the function body has not run yet. await greet() or asyncio.run(greet()) actually executes the body. Forgetting to await a coroutine is a common bug — the coroutine object is created but the code never runs. Python 3.11+ warns about unawaited coroutines.await inside an async def function — never at the top level of a module or inside a regular function. In FastAPI, all route handlers that do async work must be defined as async def. The rule is simple: if you need to await anything, the function must be async def. If you do not await anything, use a regular def (FastAPI runs it in a thread pool automatically).await does NOT mean “run this concurrently.” It means “wait for this coroutine to finish before continuing.” Sequential await calls are still sequential — they just yield to the event loop while waiting instead of blocking the thread. To run coroutines concurrently, use asyncio.gather() or asyncio.create_task() (covered in the next lesson).The Awaitable Protocol
import asyncio
# You can await: coroutines, Tasks, Futures, and objects with __await__
# ── Coroutine ─────────────────────────────────────────────────────────────────
async def greet(name: str) -> str:
await asyncio.sleep(0.1)
return f"Hello, {name}!"
async def main():
# Sequential — greet() runs, then we proceed
msg = await greet("Alice")
print(msg) # "Hello, Alice!"
# Chain awaits
user = await fetch_user(1)
posts = await fetch_posts(user["id"])
return posts
# ── asyncio.sleep() — the async equivalent of time.sleep() ────────────────────
async def demo():
print("Start")
await asyncio.sleep(1.0) # yield for 1 second — other coroutines can run
print("After 1 second")
# ── What happens step by step ─────────────────────────────────────────────────
# 1. asyncio.run(main()) creates the event loop and schedules main()
# 2. main() runs until it hits 'await greet("Alice")'
# 3. greet() runs until it hits 'await asyncio.sleep(0.1)'
# 4. Sleep yields to event loop — event loop runs other pending coroutines
# 5. After 0.1s, sleep completes, greet() resumes, returns "Hello, Alice!"
# 6. main() resumes with msg = "Hello, Alice!"
async/await with Real I/O
import asyncio
import httpx
# ── Async HTTP with httpx ──────────────────────────────────────────────────────
async def fetch_post(post_id: int) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(f"https://jsonplaceholder.typicode.com/posts/{post_id}")
response.raise_for_status()
return response.json()
async def main():
post = await fetch_post(1)
print(post["title"])
asyncio.run(main())
# ── Async database (asyncpg example) ──────────────────────────────────────────
import asyncpg
async def get_users_from_db(pool: asyncpg.Pool) -> list:
async with pool.acquire() as connection:
rows = await connection.fetch("SELECT id, name, email FROM users LIMIT 10")
return [dict(row) for row in rows]
async def setup():
pool = await asyncpg.create_pool(dsn="postgresql://localhost/mydb")
users = await get_users_from_db(pool)
await pool.close()
return users
Rules of async/await
| Rule | Correct | Wrong |
|---|---|---|
| await inside async def only | async def f(): await coro() |
def f(): await coro() → SyntaxError |
| Call async function with await | result = await async_func() |
result = async_func() → coroutine object |
| Run from sync code | asyncio.run(main()) |
main() → nothing runs |
| Can call sync from async | async def f(): sync_func() |
Only avoid if sync_func blocks I/O |
| await blocks current coro only | Other coroutines can run during await | Does NOT block other coroutines |
Common Mistakes
Mistake 1 — Forgetting to await a coroutine
❌ Wrong — coroutine object created but never executed:
async def main():
result = fetch_data(1) # forgot await — result is a coroutine, not data!
print(result) # <coroutine object fetch_data ...>
✅ Correct:
async def main():
result = await fetch_data(1) # ✓ actually runs and returns data
print(result)
Mistake 2 — Using await outside async def
❌ Wrong — SyntaxError:
def process():
data = await fetch_data(1) # SyntaxError: 'await' outside async function
✅ Correct — make the function async:
async def process():
data = await fetch_data(1) # ✓
Mistake 3 — Confusing sequential await with concurrent execution
❌ Wrong — expects both to run simultaneously:
async def main():
result1 = await fetch(url1) # waits for url1 to complete
result2 = await fetch(url2) # THEN waits for url2 — still sequential!
✅ Correct — use asyncio.gather() for concurrent execution (next lesson):
async def main():
result1, result2 = await asyncio.gather(fetch(url1), fetch(url2)) # concurrent ✓
Quick Reference
| Pattern | Code |
|---|---|
| Async function | async def f(): ... |
| Await a coroutine | result = await coro() |
| Run from sync | asyncio.run(main()) |
| Non-blocking sleep | await asyncio.sleep(seconds) |
| Async HTTP GET | await httpx.AsyncClient().get(url) |
| Check if coroutine | asyncio.iscoroutine(obj) |
| Check if coroutine func | asyncio.iscoroutinefunction(func) |