Async/Await (asyncio)

Difficulty: Advanced

Question

Explain async/await in Python. How does asyncio work?

Answer

asyncio is Python's built-in library for writing concurrent code using async/await syntax.

Key concepts: - Coroutine: a function defined with 'async def', returns a coroutine object - await: suspends execution until the awaited coroutine completes - Event loop: runs coroutines, handles I/O events, manages tasks - Task: a wrapper that schedules a coroutine to run concurrently - asyncio.gather(): run multiple coroutines concurrently

async/await is ideal for I/O-bound operations (HTTP requests, database queries, file I/O) where you spend most time waiting. It uses a single thread with cooperative multitasking.

Code examples

Basic async/await

import asyncio

async def greet(name, delay):
    """A coroutine that simulates async work."""
    print(f"Starting greeting for {name}")
    await asyncio.sleep(delay)  # Non-blocking sleep
    print(f"Hello, {name}! (after {delay}s)")
    return f"Greeted {name}"

async def main():
    # Sequential: 3 seconds total
    print("=== Sequential ===")
    result1 = await greet("Alice", 1)
    result2 = await greet("Bob", 2)
    print(f"Results: {result1}, {result2}")
    
    # Concurrent: 2 seconds total (max of delays)
    print("\n=== Concurrent ===")
    results = await asyncio.gather(
        greet("Charlie", 1),
        greet("Diana", 2),
        greet("Eve", 1.5),
    )
    print(f"Results: {results}")

asyncio.run(main())

Sequential await blocks until each coroutine finishes. asyncio.gather() runs them concurrently - total time is the maximum, not the sum.

Tasks and Error Handling

import asyncio

async def fetch_data(url, delay):
    print(f"Fetching {url}...")
    await asyncio.sleep(delay)
    if 'error' in url:
        raise ValueError(f"Failed to fetch {url}")
    return f"Data from {url}"

async def main():
    # Create tasks for concurrent execution
    task1 = asyncio.create_task(fetch_data('api/users', 1))
    task2 = asyncio.create_task(fetch_data('api/posts', 2))
    
    # Tasks start immediately, await collects results
    result1 = await task1
    result2 = await task2
    print(f"Got: {result1}, {result2}")
    
    # Error handling with gather
    results = await asyncio.gather(
        fetch_data('api/ok', 1),
        fetch_data('api/error', 1),
        return_exceptions=True  # Don't raise, return exceptions
    )
    for r in results:
        if isinstance(r, Exception):
            print(f"Error: {r}")
        else:
            print(f"Success: {r}")
    
    # Timeout
    try:
        result = await asyncio.wait_for(
            fetch_data('api/slow', 10),
            timeout=2.0
        )
    except asyncio.TimeoutError:
        print("Request timed out!")

asyncio.run(main())

create_task() starts a coroutine running immediately. return_exceptions=True in gather prevents one failure from canceling others. wait_for() adds timeouts.

Async Context Managers and Iterators

import asyncio

# Async context manager
class AsyncDatabase:
    async def __aenter__(self):
        print("Connecting to database...")
        await asyncio.sleep(0.5)  # Simulate connection
        print("Connected!")
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("Closing connection...")
        await asyncio.sleep(0.1)
        print("Connection closed.")
        return False
    
    async def query(self, sql):
        await asyncio.sleep(0.2)
        return f"Results for: {sql}"

# Async iterator
class AsyncCounter:
    def __init__(self, limit):
        self.limit = limit
        self.count = 0
    
    def __aiter__(self):
        return self
    
    async def __anext__(self):
        if self.count >= self.limit:
            raise StopAsyncIteration
        self.count += 1
        await asyncio.sleep(0.1)
        return self.count

async def main():
    # Async with
    async with AsyncDatabase() as db:
        result = await db.query("SELECT * FROM users")
        print(result)
    
    # Async for
    async for num in AsyncCounter(5):
        print(f"Count: {num}", end=' ')
    print()
    
    # Async comprehension
    values = [num async for num in AsyncCounter(3)]
    print(f"Values: {values}")

asyncio.run(main())

async with uses __aenter__/__aexit__ for async resource management. async for uses __aiter__/__anext__ for async iteration. Both are essential for async database and HTTP patterns.

Key points

Concepts covered

asyncio, async/await, Coroutines, Tasks, Event Loop, aiohttp