Python Async / Await Complete

Asynchronous Python

Do thousands of I/O operations concurrently on a single thread. Master the event loop, coroutines, async/await, asyncio, and the patterns that power modern Python servers.

Python 3.7+ Concurrency 8 Topics Interview Key
01

Why Async? Blocking vs Non-Blocking

Async lets a single thread juggle many tasks that spend most of their time waiting — network calls, disk reads, database queries. Instead of sitting idle during a wait, the program switches to other work. This is concurrency, not parallelism: one worker, never idle, not many workers at once.

Synchronous (blocking)

Each call must finish before the next starts. Download 3 pages at 1s each → 3 seconds. The CPU sleeps while the network works.

Asynchronous (non-blocking)

Start all 3 downloads, await them together. Total ≈ 1 second. While one waits on I/O, the loop runs the others.

When to reach for async

I/O-bound work (APIs, sockets, DB, files) → asyncio shines. CPU-bound work (number crunching, image processing) → use multiprocessing instead; async won't help because there's no waiting to overlap.

02

Coroutines & the await Keyword

A coroutine is a function defined with async def. Calling it does not run it — it returns a coroutine object. You must await it (or schedule it) to actually execute. await hands control back to the event loop until the awaited thing is ready.

Python — Your First Coroutine
import asyncio

async def greet(name):
    print(f"Hello {name}")
    await asyncio.sleep(1)      # non-blocking pause — loop runs others
    print(f"Bye {name}")
    return f"done: {name}"

greet("Ada")              #  returns a coroutine — does NOT run
asyncio.run(greet("Ada"))  #  runs it on the event loop
async def
Defines a coroutine function.
await
Suspends the coroutine, yields control to the loop until the awaitable completes.
Awaitable
A coroutine, Task, or Future — anything you can await.
Rule
await is only legal inside an async def.
03

The Event Loop

The event loop is the engine. It maintains a queue of ready tasks, runs one until it hits an await that isn't ready, parks it, and runs the next. When I/O completes, the parked task is re-queued. asyncio.run() creates the loop, runs your main coroutine to completion, then closes it.

Python — Entry Point
import asyncio

async def main():
    print("start")
    await asyncio.sleep(1)
    print("end")

# Python 3.7+ — the modern, only entry point you need
asyncio.run(main())

# Inside a running loop, grab it with:
# loop = asyncio.get_running_loop()

One loop, one thread

There is normally one event loop per thread. Never call asyncio.run() from inside a coroutine — it's a top-level entry point. Don't run blocking code (like time.sleep or heavy CPU) on the loop; it freezes everything.

04

Tasks & asyncio.gather

Awaiting coroutines one by one is still sequential. To run them concurrently, wrap them in Tasks (which schedule them on the loop immediately) or use asyncio.gather() to launch and collect many at once.

Python — Sequential vs Concurrent
import asyncio, time

async def work(n):
    await asyncio.sleep(1)
    return n * n

async def main():
    #  Sequential — ~3s total
    a = await work(1); b = await work(2); c = await work(3)

    #  Concurrent — ~1s total
    results = await asyncio.gather(work(1), work(2), work(3))
    print(results)               # [1, 4, 9] — order preserved

    #  Or schedule explicit Tasks
    t1 = asyncio.create_task(work(4))
    t2 = asyncio.create_task(work(5))
    print(await t1, await t2)    # 16 25

asyncio.run(main())
ToolWhat it doesUse when
create_task()Schedules a coroutine to run now, returns a TaskYou need a handle to await/cancel later
gather(*aws)Runs all concurrently, returns a list of results in orderYou want all results together
as_completed()Yields results as each finishesYou want to process whichever finishes first
TaskGroup()Structured concurrency (3.11+), auto-cancels siblings on errorModern, safest grouping
05

Concurrency Patterns

Timeouts, racing for the first result, and limiting how many run at once are the everyday tools of async code.

Python — Timeout, as_completed, Semaphore
import asyncio

# 1. Timeout — cancel if it takes too long
try:
    await asyncio.wait_for(slow_call(), timeout=2.0)
except asyncio.TimeoutError:
    print("too slow!")

# 2. Process results as they arrive
for fut in asyncio.as_completed([work(1), work(2)]):
    print(await fut)

# 3. Limit concurrency — at most 5 in flight
sem = asyncio.Semaphore(5)
async def guarded(url):
    async with sem:
        return await fetch(url)
06

Async HTTP with aiohttp

The requests library is blocking — using it in async code stalls the loop. aiohttp is the async HTTP client. Fetch hundreds of URLs concurrently with one shared session.

Python — Fetch Many URLs Concurrently
import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        return await resp.text()

async def main(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, u) for u in urls]
        pages = await asyncio.gather(*tasks)
        print(f"fetched {len(pages)} pages")

asyncio.run(main(["https://example.com"] * 50))

Reuse the session

Create one ClientSession and share it across requests — it pools connections. Creating a session per request is a classic performance bug.

07

Real-World Pattern — Producer / Consumer

An asyncio.Queue decouples producers from consumers — the backbone of crawlers, job runners, and pipelines. Workers pull items concurrently while producers push.

Python — Queue with Worker Pool
import asyncio

async def worker(name, queue):
    while True:
        job = await queue.get()
        await asyncio.sleep(0.5)        # pretend to work
        print(f"{name} did {job}")
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    for i in range(10):
        queue.put_nowait(f"job-{i}")

    workers = [asyncio.create_task(worker(f"w{n}", queue)) for n in range(3)]
    await queue.join()            # wait until every job is done
    for w in workers: w.cancel()  # stop idle workers

asyncio.run(main())
08

Common Pitfalls

Forgetting await

greet("x") creates a coroutine that never runs and emits a "coroutine was never awaited" warning. Always await it or wrap in a task.

Blocking the loop

Calling time.sleep(), requests.get(), or heavy CPU work freezes every coroutine. Use asyncio.sleep, aiohttp, or offload with loop.run_in_executor().

await in a loop = sequential

for u in urls: await fetch(u) runs one at a time. Build a list of tasks and gather them for real concurrency.

Async isn't parallel

It's single-threaded cooperative multitasking. For CPU-bound work, reach for multiprocessing or concurrent.futures.ProcessPoolExecutor.

Quick Quiz

1. Calling an async def function directly returns…
2. To run three coroutines concurrently you use…
3. Which call blocks the event loop?
4. asyncio is best suited for…
5. The modern entry point for an async program is…