Python Async/Await — Core Concepts

async/await is Python’s model for cooperative concurrency. It helps one process handle many I/O-bound tasks by switching work whenever a task hits a wait point.

Why Async Exists

A lot of application time is spent waiting:

  • HTTP requests
  • database responses
  • disk/network I/O
  • queue operations

If code waits in a blocking style, each worker sits idle during that wait. To scale, you add more threads or processes, which raises memory use and coordination complexity.

Async offers a different tradeoff: one thread, one event loop, many coroutines that voluntarily yield control during waits.

Core Building Blocks

async def creates a coroutine function

async def fetch_user(user_id):
    ...

Calling it returns a coroutine object. Nothing runs until it is awaited or scheduled.

await pauses at an awaitable

user = await fetch_user(42)

At await, control returns to the event loop, which can run other ready tasks.

Event loop

The event loop is the scheduler. It tracks pending tasks, wakes them when I/O completes, and advances each coroutine to its next await.

Running Coroutines

The modern top-level entry is asyncio.run.

import asyncio

async def main():
    ...

asyncio.run(main())

Inside main, launch concurrent work with asyncio.gather.

results = await asyncio.gather(
    fetch_user(1),
    fetch_user(2),
    fetch_user(3),
)

gather runs them concurrently and returns results in call order.

Practical Example: Many API Calls

import asyncio
import httpx

async def fetch_json(client, url):
    r = await client.get(url, timeout=10)
    r.raise_for_status()
    return r.json()

async def main(urls):
    async with httpx.AsyncClient() as client:
        tasks = [fetch_json(client, u) for u in urls]
        return await asyncio.gather(*tasks)

If each request takes ~300 ms and you need 100 of them, sequential code can take ~30 seconds. Async can bring wall-clock time much closer to the slowest group of outstanding requests, limited by remote systems and your own concurrency cap.

Concurrency Control

Unbounded concurrency can overload your service, hit rate limits, or exhaust sockets. Use a semaphore.

sem = asyncio.Semaphore(20)

async def bounded_fetch(client, url):
    async with sem:
        return await fetch_json(client, url)

Now only 20 requests run at once.

Timeouts, Cancellation, and Cleanup

Reliable async code treats cancellation as normal, not exceptional chaos.

  • use deadlines/timeouts (asyncio.timeout in modern Python)
  • handle asyncio.CancelledError for cleanup
  • close network clients in async with
import asyncio

async def worker():
    try:
        await asyncio.sleep(60)
    except asyncio.CancelledError:
        # release resources, flush metrics, etc.
        raise

Common Misconception

Misconception: async makes Python CPU work faster.

Reality: async improves throughput for I/O-bound workloads. CPU-bound tasks still compete under the GIL in one process. For heavy CPU jobs, use multiprocessing or native extensions.

When to Choose Async

Great fit:

  • API gateways and microservices
  • chat/websocket servers
  • crawlers and scrapers
  • message consumers with network-heavy steps

Less ideal:

  • short scripts with tiny I/O
  • CPU-heavy image/video/data crunching
  • teams without appetite for async-aware libraries and debugging patterns

Relationship to Other Python Topics

If you already know Python Generators and Python Iterators, async coroutines feel familiar: both involve pause/resume behavior. The difference is that coroutines are orchestrated by an event loop and are designed for awaitable I/O.

One Thing to Remember

Async/await is a scheduling model for I/O waits: coroutines pause at await, the event loop runs other tasks, and your app keeps progressing instead of idling.

pythonasync-awaitevent-loopio

See Also

  • Python Basics Python is the programming language that reads like plain English — here's why millions of beginners (and experts) choose it first.
  • Python Booleans Make Booleans click with one clear analogy you can reuse whenever Python feels confusing.
  • Python Break Continue Make Break Continue click with one clear analogy you can reuse whenever Python feels confusing.
  • Python Closures See how Python functions can remember private information, even after the outer function has already finished.
  • Python Comprehensions See how Python lets you build new lists, sets, and mini lookups in one clean line instead of messy loops.