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.