← Back to Python series
🚀
Advanced
async/await · Event loop · gather · aiohttp · asyncio.Queue

Week 5 — Asyncio

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

asyncioasyncawaitcoroutineaiohttpevent loop
Duration
3 hours
Level
📊 Advanced
Prerequisite
🎯 Week 4
OUTCOME
Build an async web scraper that fetches 50 URLs concurrently

What you'll learn

  • 1Define coroutines with async def and await
  • 2Run coroutines with asyncio.run() and create tasks
  • 3Fetch multiple URLs concurrently with asyncio.gather
  • 4Limit concurrency with asyncio.Semaphore
  • 5Use asyncio.Queue for async producer-consumer

1. Async Basics

python
import asyncio

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

async def main():
    # Run concurrently
    await asyncio.gather(
        greet("Alice", 2),
        greet("Bob", 1),
        greet("Carol", 0.5),
    )
    # Prints Carol, Bob, Alice (in order of completion)

asyncio.run(main())

2. Tasks & Cancellation

python
import asyncio

async def background_task():
    for i in range(100):
        print(f"  bg: step {i}")
        await asyncio.sleep(0.1)

async def main():
    task = asyncio.create_task(background_task())
    await asyncio.sleep(0.3)   # let it run for 0.3s
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("Task cancelled")

asyncio.run(main())

3. aiohttp

python
import asyncio, aiohttp

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

async def main(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, u) for u in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)
    for url, result in zip(urls, results):
        if isinstance(result, Exception):
            print(f"FAIL {url}: {result}")
        else:
            status, size = result
            print(f"{status} {url} ({size} chars)")

💻 Examples

Run these examples and check the output yourself.

01_async_gather.pyConcurrent API simulation with gather
CODE
import asyncio, random

async def fetch_data(item_id: int) -> dict:
    delay = random.uniform(0.1, 0.5)
    await asyncio.sleep(delay)
    return {"id": item_id, "value": item_id * 10, "delay": round(delay, 2)}

async def main():
    import time
    start = time.perf_counter()
    results = await asyncio.gather(*[fetch_data(i) for i in range(1, 11)])
    elapsed = time.perf_counter() - start
    for r in results:
        print(f"  id={r['id']} value={r['value']} fetched_in={r['delay']}s")
    print(f"Total time: {elapsed:.2f}s (sequential would be ~{sum(r['delay'] for r in results):.2f}s)")

asyncio.run(main())

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

Rate-limited async downloader

Goal: Fetch N URLs concurrently but limit to max 5 simultaneous requests using asyncio.Semaphore.

Requirements
  • Use asyncio.Semaphore(5) to limit concurrency
  • Collect results in order (use tasks, not gather directly)
  • Print status code and response size for each URL
Toggle solution
SOLUTION
import asyncio
import aiohttp

async def fetch(session, url, sem):
    async with sem:
        try:
            async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
                body = await r.text()
                return url, r.status, len(body)
        except Exception as e:
            return url, 0, 0

async def main(urls):
    sem = asyncio.Semaphore(5)
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, u, sem) for u in urls]
        for coro in asyncio.as_completed(tasks):
            url, status, size = await coro
            print(f"[{status}] {url} ({size} chars)")

urls = [f"https://httpbin.org/delay/{i%3}" for i in range(10)]
asyncio.run(main(urls))
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗