Concurrency vs Parallelism
These two terms are frequently confused. Understanding the difference tells you which tool to pick.Concurrency (I/O-bound)
Multiple tasks make progress by taking turns. While one task waits for a response, another runs. All tasks share a single CPU core. This is
asyncio’s domain.Use cases: web requests, database queries, file I/O, WebSocket connections.Parallelism (CPU-bound)
Multiple tasks run at the exact same moment on different CPU cores. Python uses
multiprocessing for this because the GIL prevents true thread-level parallelism.Use cases: image processing, matrix math, video encoding, machine learning training.async def and await
Declaring a function with async def creates a coroutine. Calling it returns a coroutine object — nothing runs yet. To actually execute it and get the result, you await it inside another coroutine.
You can only
await inside an async def function. Regular (synchronous) functions cannot use await. If you call asyncio.run() from within a running event loop (e.g., inside a Jupyter notebook), use await main() directly instead.The Event Loop
The event loop is the scheduler that drives async code. It maintains a queue of coroutines, runs each one until it hits anawait, pauses it, and switches to the next ready task. When the I/O operation completes, the original task is resumed.
asyncio.run(). Inside async code, you never call asyncio.run() again — you just await.
Running Tasks Concurrently with asyncio.gather()
The real power of async appears when you fire off multiple tasks at the same time. asyncio.gather() takes several coroutines and runs them concurrently, returning all their results once every one has completed.
gather, each await would run sequentially (6 seconds total). With gather, all three run concurrently (3 seconds total — limited only by the slowest task).
asyncio.create_task() for Fire-and-Forget
When you need a task to start immediately without waiting for it right now, use create_task():
async for and async with
Use async for to iterate over an async generator (one that yields items over time, like a streaming database cursor) and async with to use an async context manager.
Real-World Example: Fetch Multiple API Endpoints
FastAPI and Async
FastAPI is built on Starlette, an ASGI framework, and runs on an async event loop. Everyasync def route handler you write is a coroutine that FastAPI awaits inside its event loop:
Common Pitfalls
Awaiting synchronous blocking calls
Awaiting synchronous blocking calls
await only yields control for async operations. Calling a blocking synchronous function inside an async function (e.g., time.sleep(), synchronous requests.get()) blocks the entire event loop.Forgetting to await a coroutine
Forgetting to await a coroutine
Calling an async function without
await gives you a coroutine object, not the result. Python 3 will emit a RuntimeWarning.