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)
Concurrency vs Parallelism: Concurrency means multiple tasks make progress over time (they overlap); parallelism means multiple tasks run at the exact same time (multiple CPUs). Async I/O gives concurrency without multiple threads or processes.
Single-threaded cooperative multitasking: asyncio typically runs in one thread. Tasks voluntarily yield control (with await), allowing other tasks to run during waits.
Best use case: I/O-bound workloads (HTTP calls, database queries with async drivers, web sockets). Not ideal for CPU-heavy computations.
Core Concepts
Coroutine: Defined with async def, returns a coroutine object when called. You must await it or schedule it to run.
Event loop: Schedules and executes coroutines. Start it with asyncio.run(main()).
await: Pause the current coroutine until the awaited awaitable completes; the loop runs other tasks in the meantime.
Task: A wrapper around a coroutine returned by asyncio.create_task().
Future: A low-level awaitable representing an eventual result.
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
await coro() — run coroutine and wait for result.
asyncio.create_task(coro()) — schedule task to run in background.
asyncio.gather(*coros) — run coroutines concurrently and gather results (order preserved).
asyncio.as_completed(iterable) — iterate tasks as they finish (useful to process results early).
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
Use asyncio.gather(..., return_exceptions=True) to collect exceptions without cancelling all tasks.
Python 3.11+: ExceptionGroup and except* let you handle multiple exceptions raised concurrently.
Always try/except inside coroutines for fine-grained handling.
Async Features: Iterators, Context Managers, Comprehensions
async for — iterate over async iterators.
async with — manage async context (e.g., aiohttp.ClientSession()).
Async comprehensions: [x async for x in agen()].
These constructs keep code clean and ensure connections/resources are managed non-blockingly.
Libraries & Frameworks (Useful Ecosystem)
HTTP & Networking: aiohttp, httpx (async mode), websockets
Web frameworks/ASGI: FastAPI, Starlette, Sanic, Quart, servers like uvicorn and hypercorn
DB & ORM: asyncpg, aiomysql, Motor (MongoDB), Databases, Tortoise ORM
Utilities & Testing: aiofiles, aiocache, pytest-asyncio
Best Practices
Use asyncio.run() for top-level entrypoints.
Reuse sessions (aiohttp.ClientSession) instead of recreating them.
Limit concurrency with asyncio.Semaphore() or asyncio.BoundedSemaphore when hitting external services.
Move blocking/cpu-heavy work to process pools or use asyncio.to_thread().
Clean up tasks and cancel unused tasks properly.
Test async code with pytest-asyncio.
⏱️ When to Use Asyncio vs Threading vs Multiprocessing
I/O-bound & high concurrency: Use asyncio.
I/O-bound but limited concurrency or blocking 3rd-party libs: Threading may be fine.
CPU-bound work: Use multiprocessing (ProcessPoolExecutor) or specialized libraries.
Simple rule of thumb:
if io_bound:
if io_slow:
use asyncio
else:
use threading
elif cpu_bound:
use multiprocessing
Quick Examples Recap
asyncio.gather() — run a collection concurrently.
asyncio.create_task() — schedule a background task.
asyncio.as_completed() — iterate as tasks finish.
asyncio.Queue() — producer/consumer pattern.
asyncio.to_thread() — run blocking code safely.
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.