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).