Advertisement
🚀 Advanced Python

Python Async Programming – asyncio, async/await Explained

Async programming lets a single thread handle thousands of concurrent I/O operations without blocking. Instead of waiting idle for a network request, an async program switches to other work. Python's asyncio library and async/await syntax make this efficient and readable.

⏱️ 25 min read🎯 Advanced📅 Updated 2026

async def and await – The Basics

async def creates a coroutine function. await suspends execution until the awaited operation completes.

Python
import asyncio

async def greet(name, delay):
    await asyncio.sleep(delay)  # Non-blocking wait
    print(f"Hello, {name}!")

async def main():
    # Run sequentially: takes 3s
    await greet("Alice", 1)
    await greet("Bob", 2)

asyncio.run(main())
▶ Output
Hello, Alice! Hello, Bob!

asyncio.gather – True Concurrency

Run multiple coroutines concurrently with asyncio.gather().

Python
import asyncio
import time

async def fetch(url, delay):
    await asyncio.sleep(delay)  # Simulate network I/O
    return f"Response from {url}"

async def main():
    start = time.time()

    # All three run concurrently — total ~2s not 6s
    results = await asyncio.gather(
        fetch("api.com/users", 2),
        fetch("api.com/posts", 1),
        fetch("api.com/data",  2)
    )

    for r in results:
        print(r)
    print(f"Done in {time.time()-start:.1f}s")

asyncio.run(main())
▶ Output
Response from api.com/users Response from api.com/posts Response from api.com/data Done in 2.0s

asyncio.create_task – Background Tasks

Tasks run in the background. Useful for fire-and-forget operations.

Python
import asyncio

async def background_worker():
    while True:
        print("Background: heartbeat")
        await asyncio.sleep(5)

async def main():
    task = asyncio.create_task(background_worker())

    # Do other work while background runs
    await asyncio.sleep(0)   # Yield control
    print("Main: doing work")
    task.cancel()            # Stop background

asyncio.run(main())
Advertisement

Real Async HTTP with aiohttp

Use aiohttp for async HTTP requests. Dramatically faster than requests for concurrent fetching.

Python
# pip install aiohttp
import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.json()

async def main():
    urls = [
        "https://api.github.com/users/python",
        "https://api.github.com/users/torvalds"
    ]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    for r in results:
        print(r["login"])

asyncio.run(main())
▶ Output
python torvalds

async/await: Concurrency Without Threads

asyncio runs many tasks on a single thread by letting each one pause at an await and hand control back to the event loop. It's cooperative: a task only yields when it hits await, so one CPU-heavy loop with no await blocks everything.

import asyncio

async def fetch(name, delay):
    await asyncio.sleep(delay)      # yields to the loop — other tasks run
    return f"{name} done"

async def main():
    # run concurrently — total time ≈ the SLOWEST, not the sum
    results = await asyncio.gather(fetch("a", 2), fetch("b", 1))
    print(results)

asyncio.run(main())
WorkloadUse
I/O-bound (network, disk, DB)asyncio — huge win
CPU-bound (crunching numbers)multiprocessing — asyncio won't help

The catch: async only speeds up waiting. Because of the GIL and the single thread, CPU-bound work still runs serially — and a blocking call (time.sleep, a sync DB driver) freezes the whole loop. Use await asyncio.sleep() and async libraries throughout, or offload heavy work with run_in_executor.

🏋️ Practical Exercise

Get comfortable with asyncio:

  1. Write an async def coroutine that awaits asyncio.sleep(1) and prints a message.
  2. Run it with asyncio.run().
  3. Use asyncio.gather() to run three such coroutines concurrently and time the total.
  4. Schedule one coroutine as a background task with asyncio.create_task().

🔥 Challenge Exercise

Simulate fetching data from five “endpoints”, each represented by a coroutine that sleeps a random amount and returns a result. Run them sequentially and time it, then run them concurrently with asyncio.gather() and compare the total time — demonstrating that concurrency overlaps the waiting. Bonus: add a timeout with asyncio.wait_for() so a slow endpoint is cancelled.

📋 Summary

  • Async programming runs many I/O-bound tasks concurrently on a single thread via an event loop.
  • A coroutine is defined with async def and paused/resumed with await.
  • asyncio.run() starts the event loop and runs a coroutine to completion.
  • asyncio.gather() runs multiple coroutines concurrently and collects their results.
  • asyncio.create_task() schedules a coroutine to run in the background.
  • Async helps with I/O-bound work (network, disk); for CPU-bound work use multiprocessing.

Interview Questions on Async / Asyncio

  • What is asynchronous programming and how does it differ from threading?
  • What do the async and await keywords do?
  • What is a coroutine?
  • What is the difference between concurrency and parallelism?
  • What does asyncio.gather() do?
  • When is async I/O beneficial versus when does it not help?
  • What is an event loop?

FAQ

What is the difference between async and threading? +

Async uses a single thread and an event loop that switches between tasks at await points — cooperative multitasking with no lock overhead. Threading uses multiple OS threads preempted by the scheduler. Async excels at many concurrent I/O waits; threads can run blocking code without rewriting it.

When does async actually make my code faster? +

When the work is I/O-bound — network requests, database calls, file access — where tasks spend most time waiting. Async overlaps that waiting. For CPU-bound work it gives no speedup because only one coroutine runs at a time.

Why do I get “coroutine was never awaited”? +

Calling an async def function returns a coroutine object; it does not run until awaited or scheduled. Use await coro(), asyncio.run(coro()), or asyncio.create_task(coro()).

Can I mix blocking code with asyncio? +

A blocking call inside a coroutine stalls the whole event loop. Offload it with loop.run_in_executor() or asyncio.to_thread() so the loop stays responsive.