asyncio
Python asyncio — The Complete Guide
Learn Python's built-in asyncio library: the event loop, coroutines, tasks, gather, and how to write fast non-blocking code.
Introduction
asyncio is Python's built-in library for writing asynchronous, concurrent code using the
async/await syntax. It provides an event loop that runs many coroutines on a single thread,
letting your program handle thousands of I/O operations — network calls, file access, database queries — without blocking or
spinning up expensive threads.
asyncio lets a single Python thread juggle many I/O-bound tasks concurrently using an event loop.
Real-World Analogy
The Restaurant Waiter
One waiter (the event loop) serves many tables. They take an order at table 1, and while the kitchen cooks, they take orders
at tables 2 and 3 instead of standing idle. asyncio is that waiter — one worker efficiently switching between
many tasks whenever one is "waiting."
Prerequisites
Before You Start
- Python 3.7+ installed (3.11+ recommended for the latest asyncio features)
- Understanding of functions and return values
- Familiarity with
async/awaitand coroutines - Basic idea of blocking vs non-blocking I/O
- Optional:
pip install aiohttpfor async HTTP examples
Core Concepts of asyncio
Event Loop
The heart of asyncio.
It schedules and runs coroutines, resuming each one whenever its awaited operation is ready. asyncio.run() creates and manages it for you.
Coroutine
Defined with async def.
A special function you can pause with await. Calling it returns a coroutine object that must be awaited or scheduled to actually run.
Task
A coroutine scheduled to run concurrently.
Created with asyncio.create_task(), a Task starts running in the background on the event loop immediately.
Your First asyncio Program
Every asyncio program starts with an async def coroutine, launched by asyncio.run().
import asyncio
async def main():
print("Hello")
await asyncio.sleep(1) # non-blocking pause for 1 second
print("World")
asyncio.run(main()) # creates the event loop and runs main()
asyncio.sleep() is non-blocking — during that second, the event loop can run other tasks.
Running Tasks Concurrently
The real power of asyncio appears when you run multiple coroutines at once.
Sequential (slow)
import asyncio
async def task(name, seconds):
print(f"Start {name}")
await asyncio.sleep(seconds)
print(f"Done {name}")
async def main():
await task("A", 2) # waits 2s
await task("B", 2) # then another 2s -> total ~4s
asyncio.run(main())
Concurrent with gather (fast)
import asyncio
async def task(name, seconds):
print(f"Start {name}")
await asyncio.sleep(seconds)
print(f"Done {name}")
return name
async def main():
# Both run at the same time -> total ~2s, not 4s
results = await asyncio.gather(
task("A", 2),
task("B", 2),
)
print(results)
asyncio.run(main())
Using create_task
import asyncio
async def worker(n):
await asyncio.sleep(n)
return n * 10
async def main():
# Schedule tasks to start immediately
t1 = asyncio.create_task(worker(1))
t2 = asyncio.create_task(worker(2))
# Do other work here if needed...
print(await t1, await t2) # collect results
asyncio.run(main())
Essential asyncio Functions
| Function | Purpose |
|---|---|
asyncio.run(coro) |
Entry point — creates the event loop, runs the coroutine, then closes the loop |
asyncio.sleep(s) |
Non-blocking pause for s seconds |
asyncio.gather(*coros) |
Run many coroutines concurrently and collect all results |
asyncio.create_task(coro) |
Schedule a coroutine to run in the background as a Task |
asyncio.wait_for(coro, timeout) |
Await with a timeout; raises TimeoutError if exceeded |
asyncio.Queue() |
Async-safe queue for producer/consumer patterns |
Real Example: Concurrent HTTP Requests
Fetching many URLs concurrently is the classic asyncio use case.
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
data = await response.text()
print(f"{url} -> {len(data)} bytes")
return data
async def main():
urls = [
"https://example.com",
"https://python.org",
"https://github.com",
]
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
await asyncio.gather(*tasks) # all three run concurrently
asyncio.run(main())
Timeouts and Cancellation
Protect against operations that hang using wait_for.
import asyncio
async def slow_task():
await asyncio.sleep(10)
return "finished"
async def main():
try:
result = await asyncio.wait_for(slow_task(), timeout=3)
print(result)
except asyncio.TimeoutError:
print("Task took too long and was cancelled")
asyncio.run(main())
The Performance Win
For \(N\) I/O tasks each taking time \(t\), running them sequentially costs:
\[ T_{\text{sequential}} = N \times t \]
Running them concurrently with asyncio.gather, the total is close to the longest single task:
\[ T_{\text{gather}} \approx \max_{1 \le i \le N} t_i \]
So 100 requests of 1 second each drop from ~100 seconds to roughly ~1 second — that's the asyncio advantage.
asyncio vs Threading vs Multiprocessing
| Approach | Best For | Note |
|---|---|---|
asyncio |
I/O-bound, high concurrency | Single thread, cooperative, very lightweight |
threading |
I/O-bound, blocking libraries | Limited by the GIL for CPU work |
multiprocessing |
CPU-bound work | True parallelism across cores; higher overhead |
Best Practices
Do This
- Use
asyncio.run()as the single entry point of your program - Use
gatherorcreate_taskto run independent coroutines concurrently - Never call blocking functions (like
time.sleeporrequests.get) inside a coroutine - Use async libraries (
aiohttp,asyncpg,aiofiles) for I/O - Always add timeouts with
wait_forto avoid hangs - Offload CPU-bound work with
loop.run_in_executoror multiprocessing
Common Mistakes
time.sleep(2) inside a coroutine — it blocks the entire event loop, freezing all tasks.
await asyncio.sleep(2) — it yields control so other tasks can run.
get_data() alone does nothing and warns "coroutine was never awaited".
Interview Questions
| Question | Short Answer |
|---|---|
| What is asyncio? | Python's standard library for asynchronous concurrency using an event loop and async/await. |
| Difference between a coroutine and a task? | A coroutine is defined with async def; a task is a scheduled coroutine running on the loop. |
What does asyncio.gather do? |
Runs multiple coroutines concurrently and returns all their results. |
Why not use time.sleep in async code? |
It blocks the event loop; use await asyncio.sleep instead. |
| Is asyncio good for CPU-bound work? | No — it's for I/O-bound concurrency; use multiprocessing for CPU work. |
Quick Revision
| Task | asyncio Way |
|---|---|
| Run the program | asyncio.run(main()) |
| Pause without blocking | await asyncio.sleep(s) |
| Run many concurrently | await asyncio.gather(*coros) |
| Background task | asyncio.create_task(coro) |
| Add a timeout | await asyncio.wait_for(coro, t) |
Key Takeaways
asyncio powers high-concurrency I/O in Python on a single thread using an
event loop. Define coroutines with async def, run them concurrently with
gather / create_task, and always use async libraries — never blocking calls — inside them.