Async Await
Asynchronous programming is at the heart of modern Python applications. Whether you're building high-performance web APIs, data pipelines, websocket apps, scrapers, or microservices — mastering advanced async/await patterns gives you the ability to build systems that scale effortlessly.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
This lesson dives beyond the basics. You'll learn event loop mechanics, tasks, concurrency patterns, async iterators, async generators, synchronization primitives, and real-world architectures used in production environments.
The Python async system is powered by the event loop , a scheduler that:
Coroutines can call other coroutines using await :
You can chain dozens of async functions without blocking the thread.
This is how you run coroutines in parallel (non-blocking):
Tasks let coroutines run independently in the background:
Different from gather() , wait() lets you specify:
Critical for resilient systems that depend on external APIs.
Used for resources that need async setup AND cleanup :
These are perfect for streaming data where each item needs async fetching:
Prevents race conditions when multiple tasks access shared data:
A professional engineer must know when to choose which.
This lets you send 1000 requests concurrently without blocking.
FastAPI, Starlette, aiohttp, Quart — all rely on:
Async lets each component run without blocking any other.
Tasks can be cancelled, but you must handle the cancellation gracefully:
If you don't handle CancelledError, tasks become "dangling zombies" and corrupt state.
TaskGroups improve error handling and cancellation. If one task inside a group fails, the entire group is safely cancelled.
TaskGroups will replace asyncio.gather() in most future architectures.
Case 2 — fail safe (continue, collect errors):
Used in microservices to avoid hammering broken APIs.
Deadlocks occur when tasks wait on each other incorrectly.
Async queues give safe producer/consumer flow.
Queues stop you from overwhelming your system.
Example — streaming Bitcoin prices, logs, or live chat updates:
Your code must yield control to let other tasks run:
Sometimes you must call blocking code inside async systems:
to_thread() prevents freezing the event loop.
Running thousands of tasks at once can overload:
For CPU-heavy workloads (AI / ML / video processing), async alone is not enough.
ProcessPoolExecutor → handles CPU-heavy tasks
This is how websocket heartbeats, game loops, monitoring tasks work.
Backpressure prevents fast producers from exploding memory.
Processing thousands of tasks at controlled speed:
Caching async functions requires async-safe patterns — you cannot use normal functools.lru_cache on coroutines.
Async LRU caching requires manual implementation:
A professional async pipeline processes streaming data in stages.
Each stage is async → memory safe → fully streaming.
WebSockets are the backbone of real-time async systems.
Many real systems run supervised workers that restart when they fail.
Sometimes you want a task to finish even if outer tasks are cancelled.
This is how modern companies scale to millions of users.
You now understand high-level async engineering concepts:
📋 Quick Reference — Async & Await
You now understand async/await patterns, how to structure concurrent code, and when to use asyncio vs threads.
Up next: AsyncIO Deep Dive — go deeper into the event loop, Tasks, and Futures.
Practice quiz
On how many OS threads does asyncio code run by default?
- One thread per coroutine
- As many threads as CPU cores
- One thread — a single event loop
- It uses processes, not threads
Answer: One thread — a single event loop. AsyncIO is single-threaded concurrency: the event loop switches between tasks on one thread.
When does the event loop switch from one task to another?
- When a task hits an await that yields control
- At random intervals
- Every 10 milliseconds
- Only when a task finishes completely
Answer: When a task hits an await that yields control. Tasks yield control at await points; with no await there is no switch (cooperative multitasking).
What does asyncio.gather(a(), b()) do when a() and b() each await asyncio.sleep(1)?
- Runs them sequentially, taking 2 seconds
- Raises an error
- Runs only the first coroutine
- Runs them concurrently, taking about 1 second
Answer: Runs them concurrently, taking about 1 second. gather runs the coroutines concurrently, so total time is ~1 second, not 2.
What is the correct way to pause for a non-blocking delay inside async code?
- time.sleep(1)
- await asyncio.sleep(1)
- wait(1)
- asyncio.pause(1)
Answer: await asyncio.sleep(1). await asyncio.sleep(1) yields control; time.sleep(1) blocks the whole event loop.
What does asyncio.create_task(coro()) do?
- Schedules the coroutine to run concurrently in the background
- Runs the coroutine immediately and blocks
- Creates a new OS thread
- Defines a new coroutine function
Answer: Schedules the coroutine to run concurrently in the background. create_task schedules the coroutine on the event loop so it runs concurrently with other code.
How do you offload a blocking CPU-heavy function without freezing the event loop?
- Call it directly inside the coroutine
- Wrap it in time.sleep()
- await asyncio.to_thread(func)
- Use await func()
Answer: await asyncio.to_thread(func). asyncio.to_thread runs blocking work in a thread so the event loop stays responsive.
What protocol methods must an async context manager (async with) implement?
- __enter__ and __exit__
- __aenter__ and __aexit__
- __next__ and __iter__
- __call__ only
Answer: __aenter__ and __aexit__. Async context managers define __aenter__ and __aexit__, used by async with.
Which exception should a task catch to handle cancellation gracefully?
- asyncio.TimeoutError
- KeyboardInterrupt
- StopAsyncIteration
- asyncio.CancelledError
Answer: asyncio.CancelledError. task.cancel() raises asyncio.CancelledError inside the task, which should be handled for clean shutdown.
AsyncIO is best suited for which kind of workload?
- CPU-bound work like math and ML training
- IO-bound work like network and file operations
- Heavy image processing
- Cryptographic hashing
Answer: IO-bound work like network and file operations. AsyncIO gives concurrency for IO-bound work; CPU-bound work needs multiprocessing.
What does passing return_exceptions=True to asyncio.gather do?
- Cancels all tasks on the first error
- Retries failed coroutines automatically
- Returns exceptions as results instead of raising them
- Disables exception handling entirely
Answer: Returns exceptions as results instead of raising them. With return_exceptions=True, gather collects exceptions into the results list instead of raising (fail-safe).