Introduction

asyncio is Python’s built-in framework for writing concurrent I/O-bound code using coroutines, an event loop, and non-blocking operations. It allows a single thread to manage multiple network calls, file actions, timers, and other waits efficiently by pausing tasks while they wait and running others in the meantime.

What is Async I/O? (Simple Explanation)

Core Concepts

Synchronous vs ⚡ Asynchronous Example

Synchronous (blocking)

import time

def count():
    print('One')
    time.sleep(1)
    print('Two')

for _ in range(3):
    count()  # ~6 seconds total

Asynchronous (non-blocking)

import asyncio

async def count():
    print('One')
    await asyncio.sleep(1)
    print('Two')

async def main():
    await asyncio.gather(count(), count(), count())  # ~2 seconds total

asyncio.run(main())

Using await releases the loop during waits, enabling concurrency and much faster overall runs for I/O-bound waits.

Running and Scheduling Tasks

Note: If you create tasks with create_task() ensure they are awaited or included in gather(); otherwise they can be cancelled when the loop ends.

Common Async Patterns

Coroutine chaining

Chain coroutines when one result is input to another (e.g., fetch user → fetch posts). Use await to pass results.

Producer–consumer with asyncio.Queue

Producers push items to an asyncio.Queue; multiple consumers fetch and process them concurrently. This decouples producers and consumers and supports scalable flows.

Example snippet:

queue = asyncio.Queue()
await queue.put(item)
item = await queue.get()

Real-world Examples

1) Fetch multiple URLs concurrently (aiohttp)

import aiohttp, asyncio

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:
        results = await asyncio.gather(*(fetch(session, u) for u in urls))
    return results

urls = ['https://example.com','https://httpbin.org/get']
results = asyncio.run(main(urls))

Best practice: reuse ClientSession() and use async with to clean up resources.

2) Process results as they arrive (as_completed)

tasks = [asyncio.create_task(fetch(session, u)) for u in urls]
for coro in asyncio.as_completed(tasks):
    res = await coro
    handle(res)  # process immediately

3) Producer/Consumer with Queue

(Producer puts user data; consumers process posts concurrently.)

4) Running blocking code safely

Use loop.run_in_executor() or asyncio.to_thread() to run CPU-bound or blocking calls outside the event loop.

import asyncio

def blocking_io():
    return sum(i*i for i in range(10_000_000))

async def main():
    result = await asyncio.to_thread(blocking_io)
    print(result)

Exception Handling & Robustness

Async Features: Iterators, Context Managers, Comprehensions


Libraries & Frameworks (Useful Ecosystem)

Best Practices

⏱️ When to Use Asyncio vs Threading vs Multiprocessing

Simple rule of thumb:

if io_bound:
    if io_slow:
        use asyncio
    else:
        use threading
elif cpu_bound:
    use multiprocessing

Quick Examples Recap

Conclusion

asyncio is a powerful, efficient concurrency model for Python applications dominated by I/O waits. Once you understand coroutines, await, and the event loop, you can build scalable services, fast web clients, and responsive systems without heavy thread pools. Combine asyncio with the rich async ecosystem (aiohttp, FastAPI, httpx) to build modern high-performance Python apps.