Python Series track

Week 05 · Advanced Track

Week 5 — Asyncio

Write asynchronous programs with asyncio. Understand coroutines, tasks, the event loop, and use aiohttp for non-blocking HTTP requests.

Advanced Track2 min readFree lesson

Level: Advanced | Prerequisite: Week 4

Learning Goals

  • Uses async/await syntax
  • Runs concurrently with asyncio.run, asyncio.gather
  • Asynchronous HTTP calls (httpx, aiohttp)
  • Producer/consumer pattern

1. Coroutine

A function defined with async def is a coroutine. Calling it does not execute immediately but returns an object.

import asyncio

async def hello():
    print("hello")
    await asyncio.sleep(1)
    print("world")

asyncio.run(hello())

2. asyncio.gather

Run multiple coroutines concurrently.

async def task(name, delay):
    await asyncio.sleep(delay)
    print(f"{name} completed")

async def main():
    await asyncio.gather(
        task("A", 1),
        task("B", 2),
        task("C", 1),
    )

asyncio.run(main())
#Approximately 2 seconds required (4 seconds for sequential)

3. Asynchronous HTTP

import asyncio
import httpx

async def fetch(client, url):
    r = await client.get(url)
    return len(r.text)

async def main():
    async with httpx.AsyncClient() as client:
        tasks = [fetch(client, "https://example.com") for _ in range(10)]
        results = await asyncio.gather(*tasks)
    print(results)

asyncio.run(main())

requests is synchronous, httpx and aiohttp are asynchronous.

4. producer / consumer

async def producer(q):
    for i in range(5):
        await q.put(i)
        print(f"Production: {i}")
        await asyncio.sleep(0.1)
    await q.put(None)   #End signal

async def consumer(q):
    while True:
        item = await q.get()
        if item is None:
            break
        print(f"Consumption: {item}")

async def main():
    q = asyncio.Queue()
    await asyncio.gather(producer(q), consumer(q))

5. Timeout

try:
    result = await asyncio.wait_for(slow_task(), timeout=1.0)
except asyncio.TimeoutError:
    print("Time out")

6. Synchronous vs Asynchronous

Threading asyncio
Unit OS thread Coroutine
Context switching Determined by OS Only in await
Memory Heavy Light
Library Mostly compatible Needs async support

Key Examples (examples/)

File Content
01_basic.py async/await basic
02_gather.py gather concurrent execution
03_queue.py Queue producer/consumer
04_timeout.py wait_for timeout

⚠️ Common Mistakes

  1. async def result is not inside await — only a coroutine object is returned, not executed.
  2. await inside a synchronous function — await can only be used inside async def.
  3. Blocking function call — Use await asyncio.sleep instead of time.sleep.
  4. Nested asyncio.run — In environments like Jupyter, there is already a loop. Use await main().

❓ FAQ

Q1. Can requests be used inside asyncio? A. It works. However, since it is blocking, there is no concurrency benefit. Use httpx.

Q2. asyncio.gather vs asyncio.wait? A. gather receives results in order and propagates on failure. wait is more flexible.

Q3. Debugging async functions is difficult. A. traceback is somewhat complex. PyCharm/VS Code debugger can help.

📝 Exercises (exercises/)

  • exercise1.md — Measure concurrent fetch time
  • exercise2.md — Queue-based task distribution
  • exercise3.md — Fetch with a timeout

Next Week

Week 6. Test

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

examples/01_basic.pyPython
import asyncio

async def hello():
    print("hello")
    await asyncio.sleep(1)
    print("world")

async def main():
    await hello()
    print("done")

asyncio.run(main())
examples/02_gather.pyPython
import asyncio

async def task(name, delay):
    print(f"{name} Start")
    await asyncio.sleep(delay)
    print(f"{name} completed")
    return name

async def main():
    results = await asyncio.gather(
        task("A", 1),
        task("B", 2),
        task("C", 1),
    )
    print("Result:", results)

import time
start = time.perf_counter()
asyncio.run(main())
print(f"Total {time.perf_counter() - start:.2f}s")
examples/03_queue.pyPython
import asyncio

async def producer(q, n):
    for i in range(n):
        await q.put(i)
        print(f"Production: {i}")
        await asyncio.sleep(0.1)
    await q.put(None)

async def consumer(q):
    while True:
        item = await q.get()
        if item is None:
            break
        print(f"Consumption: {item}")
        await asyncio.sleep(0.05)

async def main():
    q = asyncio.Queue()
    await asyncio.gather(producer(q, 5), consumer(q))

asyncio.run(main())
examples/04_timeout.pyPython
import asyncio

async def slow_task():
    await asyncio.sleep(3)
    return "Result"

async def main():
    try:
        result = await asyncio.wait_for(slow_task(), timeout=1.0)
        print(result)
    except asyncio.TimeoutError:
        print("Time Limit Exceeded (1 second)")

asyncio.run(main())

Assignment

Goal

Run fetches for 10 fake URLs concurrently vs sequentially. Compare the time.

Requirements

  • Network simulation with asyncio.sleep(0.5)
  • Run concurrently with asyncio.gather
  • Print both times (theoretically 5s vs 0.5s)

Challenge


Task 2. Queue-based task distribution

Goal

1 producer, 3 workers, 100 tasks.

Requirements

  • Producer puts 0~99 into the queue
  • 3 workers get simultaneously → process (e.g., x*x)
  • Terminate workers after all tasks are done
  • Print sum of results

Task 3. Fetch with timeout

Goal

Run 10 tasks that take random times (0.1~3 seconds) concurrently. Collect results only for those finished within 1.5 seconds.

Requirements

  • Individual timeout with asyncio.wait_for
  • Timeout marked as None
  • Output the result
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

solution1.pyPython
import asyncio
import time

async def fetch(url):
    await asyncio.sleep(0.5)
    return f"{url}: 1024 bytes"

URLS = [f"https://example.com/{i}" for i in range(10)]

async def serial():
    return [await fetch(u) for u in URLS]

async def parallel():
    return await asyncio.gather(*[fetch(u) for u in URLS])

start = time.perf_counter()
asyncio.run(serial())
print(f"Sequence: {time.perf_counter() - start:.2f}s")

start = time.perf_counter()
asyncio.run(parallel())
print(f"Simultaneous: {time.perf_counter() - start:.2f}s")
solution2.pyPython
import asyncio

async def producer(q, n):
    for i in range(n):
        await q.put(i)
    for _ in range(3):
        await q.put(None)   #3 termination signals (equal to the number of workers)

async def worker(name, q, results):
    while True:
        item = await q.get()
        if item is None:
            break
        results.append(item * item)

async def main():
    q = asyncio.Queue()
    results = []
    workers = [worker(f"w{i}", q, results) for i in range(3)]
    await asyncio.gather(producer(q, 100), *workers)
    print(f"Number of results: {len(results)}, Sum: {sum(results)}")

asyncio.run(main())
solution3.pyPython
import asyncio
import random

async def task(i):
    delay = random.uniform(0.1, 3.0)
    await asyncio.sleep(delay)
    return f"task{i} ({delay:.2f}s)"

async def with_timeout(coro, timeout):
    try:
        return await asyncio.wait_for(coro, timeout=timeout)
    except asyncio.TimeoutError:
        return None

async def main():
    coros = [with_timeout(task(i), 1.5) for i in range(10)]
    results = await asyncio.gather(*coros)
    for i, r in enumerate(results):
        print(f"{i}: {r if r else 'Timeout'}")

asyncio.run(main())

Learn this lesson with a mentor

Work through the Python Series track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now