PYTHON
Python asyncio Module: Syntax, Usage, and Examples
The asyncio module lets you run asynchronous code in Python, so your program can handle many tasks without blocking on slow operations. It’s especially useful for network requests, real-time apps, and other I/O-heavy work.
In Python async, you typically use async/await to write asynchronous functions that can pause and resume without stopping the whole program.
Learn Python on Mimo
How to Use the asyncio module in Python
The module is built around three core ideas:
- Coroutines: functions defined with
async def - Awaiting: pausing a coroutine with
awaituntil something finishes - Event loop: the engine that schedules tasks and runs them
This is a different model than purely synchronous code, where one slow operation can pause everything until it finishes.
Here’s the smallest example that runs an async function:
Python
import asyncio
asyncdefsay_hi():
print("Hi from async code!")
asyncio.run(say_hi())
asyncio.run() starts a running event loop and then cleans it up when your program finishes, so you can use asyncio without managing the loop directly.
Basic coroutine syntax
A coroutine is just a function that uses async def. Inside it, you can use await when you call other async functions.
Python
import asyncio
asyncdeffetch_profile():
await asyncio.sleep(1)
return {"name":"Mina","status":"online"}
asyncdefmain():
profile =await fetch_profile()
print(profile)
asyncio.run(main())
asyncio.sleep() is the async version of time.sleep(). The difference is huge: time.sleep() blocks the whole program, while asyncio.sleep() gives control back to the event loop so other tasks can run.
A function defined with async def is a coroutine function, and you’ll sometimes see people call it a coro.
The await keyword uses await syntax to pause the coro and yield control back to the event loop while it waits for asynchronous i/o.
Running multiple tasks at once
To run coroutines concurrently, create tasks and wait for them together.
Python
import asyncio
asyncdefdownload_file(name, seconds):
print(f"Starting {name}")
await asyncio.sleep(seconds)
print(f"Finished {name}")
asyncdefmain():
task1 = asyncio.create_task(download_file("report.pdf",2))
task2 = asyncio.create_task(download_file("photo.png",1))
await task1
await task2
asyncio.run(main())
This is already better than running them one by one. Both tasks start, and the faster one finishes first.
A scheduled task is a task object, which is a concrete thing the loop can track and cancel later if needed.
Using gather() for cleaner concurrency
asyncio.gather() runs many coroutines concurrently and collects the results.
Python
import asyncio
asyncdefget_score(player):
await asyncio.sleep(1)
returnf"{player}: 100"
asyncdefmain():
results =await asyncio.gather(
get_score("Amina"),
get_score("Kai"),
get_score("Noah")
)
print(results)
asyncio.run(main())
If you’ve ever seen a nested callback pile, this style often feels clearer because each await reads like a normal step in the flow.
When to Use the asyncio module in Python
Async code shines when your program spends time waiting. Think “waiting for the outside world”.
1) Making lots of network requests
Calling APIs and loading data from the internet can be slow. With async code, you can start many requests and process them as results arrive.
That’s perfect for:
- web scraping (responsibly)
- fetching user profiles from a service
- checking multiple endpoints
These workloads are usually i/o-bound, so async helps because it reduces time lost to waiting, not because it makes computation faster.
2) Building real-time apps
Async fits naturally in apps that stay “alive” and react to events.
Examples include:
- chat servers
- multiplayer game logic
- real-time dashboards
- bots that listen for messages
3) Handling many connections at the same time
If a server needs to handle hundreds or thousands of clients, blocking code becomes a problem quickly. Async makes it possible to keep things responsive even under load.
4) Running background I/O while doing other work
A program can continue running while waiting for:
- file reads
- database operations (through async libraries)
- incoming messages
- sensor updates
If your code is CPU-bound, asyncio won’t solve the core problem, and you may need multithreading or process-based approaches depending on the workload.
Examples of the asyncio module in Python
Below are three common patterns that show how async code behaves in practice.
Example 1: A simple timer that doesn’t block
Python
import asyncio
asyncdeftick():
for iinrange(3):
print(f"Tick {i + 1}")
await asyncio.sleep(1)
asyncio.run(tick())
This prints one tick per second, but it still leaves room for other async tasks to run.
In contrast, mixing blocking i/o into async code can freeze progress because the loop can’t switch to other tasks while it’s blocked.
Example 2: Running several “slow” tasks together
Let’s simulate multiple slow operations, like downloading data from different services.
Python
import asyncio
asyncdeffetch_data(source, seconds):
print(f"Fetching from {source}...")
await asyncio.sleep(seconds)
returnf"Data from {source}"
asyncdefmain():
results =await asyncio.gather(
fetch_data("Service A",2),
fetch_data("Service B",1),
fetch_data("Service C",3)
)
for itemin results:
print(item)
asyncio.run(main())
Even though one task takes 3 seconds, the total time is close to 3 seconds, not 6. That’s the main win.
Example 3: A repeating task using an infinite loop
Some apps need to keep checking for updates.
Python
import asyncio
asyncdefmonitor_status():
whileTrue:
print("Checking status...")
await asyncio.sleep(2)
asyncdefmain():
task = asyncio.create_task(monitor_status())
await asyncio.sleep(6)
task.cancel()
print("Stopped monitoring.")
asyncio.run(main())
This shows how a background task can run repeatedly until you stop it.
In bigger apps, it can help to log the current task name or ID when you debug cancellations and timeouts.
Learn More About the asyncio module in Python
Once you get past the basics, a few concepts come up again and again.
Await vs create_task()
await means “pause until this finishes”.
create_task() means “start this now, but don’t wait yet”.
Compare the two approaches.
Waiting one by one
Python
import asyncio
asyncdefjob(name):
await asyncio.sleep(1)
return name
asyncdefmain():
a =await job("A")
b =await job("B")
print(a, b)
asyncio.run(main())
This runs job A, then job B.
Running both at the same time
Python
import asyncio
asyncdefjob(name):
await asyncio.sleep(1)
return name
asyncdefmain():
task_a = asyncio.create_task(job("A"))
task_b = asyncio.create_task(job("B"))
results =await asyncio.gather(task_a, task_b)
print(results)
asyncio.run(main())
This runs them concurrently.
The event loop in plain language
The event loop is like a very organized manager.
- It starts tasks
- pauses tasks that are waiting
- resumes tasks when they’re ready
- keeps the program moving
You usually don’t manage the loop directly in beginner code because asyncio.run() does it for you.
The loop typically runs on the main thread, so one thread can coordinate lots of waiting work efficiently.
Futures and what “awaitable” means
Sometimes you will not await a coroutine directly, but an object that represents a pending result.
A future object is one example, and in asyncio you’ll see asyncio.future used to represent a result that will arrive later.
Common mistakes with async code
A few things trip people up early.
Mistake 1: Forgetting to await a coroutine
This creates a coroutine object but doesn’t run it.
Python
import asyncio
asyncdefgreet():
print("Hello!")
asyncdefmain():
greet()# Missing await
asyncio.run(main())
Fix:
Python
import asyncio
asyncdefgreet():
print("Hello!")
asyncdefmain():
await greet()
asyncio.run(main())
Mistake 2: Using blocking code inside async functions
Calling time.sleep() inside a coroutine blocks the event loop.
Bad:
Python
import asyncio
import time
asyncdefslow():
time.sleep(2)
print("Done")
asyncio.run(slow())
Better:
Python
import asyncio
asyncdefslow():
await asyncio.sleep(2)
print("Done")
asyncio.run(slow())
If you must call a blocking library, running it in a separate thread can keep the event loop responsive.
Handling errors in async tasks
If multiple tasks run at once, one might fail. asyncio.gather() can raise an exception when that happens.
Python
import asyncio
asyncdefrisky_job():
await asyncio.sleep(1)
raise ValueError("Something went wrong")
asyncdefsafe_job():
await asyncio.sleep(1)
return"All good"
asyncdefmain():
try:
results =await asyncio.gather(risky_job(), safe_job())
print(results)
except ValueErroras error:
print("Caught error:", error)
asyncio.run(main())
In real apps, logging these errors matters, especially when tasks run in the background.
Timeouts with wait_for()
If you don’t want to wait forever, wrap a coroutine in asyncio.wait_for().
Python
import asyncio
asyncdefslow_request():
await asyncio.sleep(5)
return"Response"
asyncdefmain():
try:
result =await asyncio.wait_for(slow_request(), timeout=2)
print(result)
except asyncio.TimeoutError:
print("Request timed out")
asyncio.run(main())
Timeouts are common in network code.
asyncio vs threading
Both can help with concurrency, but they shine in different situations.
- Async is great for I/O tasks like network calls and waiting for input.
- Threads can help when you need blocking libraries or background work that doesn’t support async.
CPU-heavy tasks, like video encoding or large math operations, usually need multiprocessing or optimized libraries, because async won’t make CPU work faster.
Summary
The asyncio module helps you write non-blocking code in Python using async def, await, and an event loop. Use it for I/O-heavy tasks like network requests, real-time applications, and handling many connections at once.
Once you understand coroutines, tasks, and tools like gather() and wait_for(), async programming starts to feel natural and can seriously speed up your workflow without adding messy complexity.
Join 35M+ people learning for free on Mimo
4.8 out of 5 across 1M+ reviews
Check us out on Apple AppStore, Google Play Store, and Trustpilot