Python Asyncio Tutorial: Master Async Programming

Programming
Date:September 18, 2026
Topic:
Python Asyncio Tutorial: Master Async Programming
⏱ 3 min read

You've written synchronous Python for years. Your code works, but it blocks on every network call, every database query, every file read. Meanwhile, your CPU sits idle. Asyncio changes that equation entirely — but only if you understand the mental model, not just the syntax.

Why Asyncio Exists

Traditional threading has two problems: the GIL prevents true parallelism for CPU-bound work, and context switching overhead kills performance at scale. Asyncio uses a single-threaded event loop with cooperative multitasking. You decide when to yield control. No race conditions. No deadlocks from locks you forgot to release. Just explicit suspension points.

"

Asyncio isn't about making code faster. It's about making waiting free.

— Guido van Rossum

The Core Primitives

Three concepts drive everything: coroutines (async def functions), tasks (scheduled coroutines), and the event loop (the scheduler). A coroutine doesn't run until awaited or wrapped in a task. The event loop runs one task until it hits await, then switches to another ready task.

python
import asyncio
import aiohttp

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

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, f'https://api.example.com/item/{i}') for i in range(100)]
        results = await asyncio.gather(*tasks)
    return results

if __name__ == '__main__':
    data = asyncio.run(main())
💡
Tipasyncio.run() creates a new event loop, runs the coroutine, and closes the loop. Use it once at your entry point. Never call it inside a running loop.

Concurrency Patterns That Matter

gather() runs everything simultaneously. But unbounded concurrency crashes servers and triggers rate limits. Use semaphores to cap parallelism:

python
async def bounded_fetch(sem, session, url):
    async with sem:
        return await fetch(session, url)

async def main():
    sem = asyncio.Semaphore(10)  # max 10 concurrent
    async with aiohttp.ClientSession() as session:
        tasks = [bounded_fetch(sem, session, url) for url in urls]
        return await asyncio.gather(*tasks)
âš ī¸
WarningNever mix blocking calls (requests, time.sleep, psycopg2) in async code. They freeze the entire event loop. Use aiohttp, asyncio.sleep, asyncpg instead.

Error Handling and Cancellation

gather() cancels all siblings on first exception by default. Use return_exceptions=True to collect results and failures together. For timeouts, wrap with asyncio.wait_for(). Cancellation propagates through await points — design your coroutines to handle CancelledError cleanly.

python
async def robust_fetch(session, url):
    try:
        return await asyncio.wait_for(fetch(session, url), timeout=5.0)
    except asyncio.TimeoutError:
        return {'url': url, 'error': 'timeout'}
    except Exception as e:
        return {'url': url, 'error': str(e)}

When Not to Use Asyncio

CPU-bound work (image processing, ML inference, heavy computation) won't benefit — the GIL still blocks. Use multiprocessing or offload to a thread pool with loop.run_in_executor(). Asyncio shines for I/O-bound workloads: web scrapers, API gateways, chat servers, database-heavy services.

Workload TypeRecommended Approach
High-latency I/O (HTTP, DB)Asyncio + async drivers
Many short I/O opsAsyncio + connection pooling
CPU-intensiveMultiprocessing or run_in_executor
Mixed I/O + CPUAsyncio for I/O, thread pool for CPU

Debugging Async Code

Enable debug mode: asyncio.run(main(), debug=True). It logs slow callbacks, unclosed resources, and exceptions never retrieved. Use asyncio.current_task().get_name() for readable traces. The aiomonitor package attaches a live REPL to a running loop — invaluable for production debugging.

â„šī¸
NotePython 3.11+ adds TaskGroups (async with TaskGroup()) for structured concurrency — automatic cleanup, exception handling, and cancellation semantics. Prefer over gather() for new code.

Your Next Steps

Pick one synchronous service you own. Identify its I/O bottlenecks. Rewrite the hot path with aiohttp/asyncpg and asyncio.Semaphore. Measure throughput before and after. You'll see 10-100x concurrency gains without adding servers. Then read the asyncio source — it's surprisingly readable and teaches the patterns better than any tutorial.

Share𝕏 Twitterin LinkedInin Whatsapp