๐ Python Asyncio Explained: Simple and Clear Guide ๐
Introduction
๐ Python Asyncio Explained: Simple and Clear Guide ๐
Introduction
Pythonโs asyncio module provides a way to write concurrent code using the async/await syntax. It allows you to run many tasks seemingly at the same time without using traditional threading or multiprocessing. This is especially useful for I/O-bound and high-level structured network code.
๐ค What is Async/Await and Coroutines?
- Async functions are defined using
async def. - These functions are called coroutines.
- Coroutines are special functions that can pause and resume their execution.
- Calling an async function does not run it immediately; it returns a coroutine object.
- To run a coroutine, you must await it, which schedules it in the event loop.
Example:
async def do_some_processing() -> str:
# Simulate some work
return "done"
# Calling the coroutine returns a coroutine object
my_coroutine = do_some_processing()
# Awaiting the coroutine runs it and gets the result
result = await my_coroutine
print(result) # Output: done
๐ Why Use Asyncio?
- Asyncio is great for managing many tasks that spend time waiting (e.g., network requests, file I/O).
- It uses a single thread and an event loop to switch between tasks efficiently.
โ๏ธ Asyncio vs Threads vs Processes:

๐ The Event Loop
- The event loop is the core of asyncio.
- It runs asynchronous tasks and callbacks, handles events, and manages coroutines.
- You can think of it as a manager that decides which coroutine runs and when.
Example:
import asyncio
async def main():
print("Start of main coroutine")
await asyncio.sleep(1)
print("End of main coroutine")
asyncio.run(main())
๐ Running Coroutines
- You can run coroutines sequentially by awaiting them one by one.
- Or run them concurrently using tasks or gather.
Example of sequential:
async def fetch_data(delay):
print("Fetching data...")
await asyncio.sleep(delay)
print("Data fetched")
return {"data": "Some Data"}
async def main():
result1 = await fetch_data(2)
result2 = await fetch_data(1)
print(result1, result2)
asyncio.run(main())
๐ Creating and Awaiting Tasks
- Tasks wrap coroutines and schedule them concurrently.
- Use
asyncio.create_task()to create a task.
โ๏ธ asyncio.create_task() โ Schedule a Coroutine to Run Concurrently
โ What is it?
asyncio.create_task() is a function that wraps a coroutine in a Task and schedules it to run concurrently in the event loop. It allows your program to start a coroutine without waiting for it to finish immediately.
Think of it like saying: โStart this job in the background, and Iโll come back to it later.โ
๐ก Why Use create_task()?
- To run multiple coroutines at the same time.
- To fire off background tasks while continuing with other work.
- To manually manage task execution and cancellation.
๐ When to Use It?
- When you want to start a coroutine and do something else immediately.
- When you need to track or cancel a task later.
- When you want fine-grained control over task execution.
โ๏ธ How Does It Work?
Hereโs a simple example:
Example:
async def fetch_data(id, delay):
print(f"Coroutine {id} starting")
await asyncio.sleep(delay)
print(f"Coroutine {id} done")
return {"id": id, "data": f"Sample data {id}"}
async def main():
task1 = asyncio.create_task(fetch_data(1, 2))
task2 = asyncio.create_task(fetch_data(2, 3))
task3 = asyncio.create_task(fetch_data(3, 1))
result1 = await task1
result2 = await task2
result3 = await task3
print(result1, result2, result3)
asyncio.run(main())
๐ง Explanation:
create_task()schedules the coroutine to run in the background.- The event loop switches between tasks as they await.
- You can
awaitthe task later to get its result.
โ ๏ธ Notes:
- If you donโt
awaitthe task or store it, it may be garbage collected before it finishes. - If a task raises an exception and you donโt handle it, it will be logged as an unhandled exception.
๐ค asyncio.gather
asyncio.gather()runs multiple coroutines concurrently and collects their results.- It returns results in the order of the coroutines passed.
- Does not cancel other coroutines if one fails.
๐ค asyncio.gather() โ Run Coroutines Concurrently and Collect Results
โ What is it?
asyncio.gather() is a high-level utility that lets you run multiple coroutines concurrently and collect their results in a single call. Itโs like sending out multiple tasks at once and waiting for all of them to return with their results.
๐ก Why Use gather()?
- To run multiple async functions at the same time.
- To collect all results in the order the coroutines were passed.
- To simplify code when you need to await many coroutines.
๐ When to Use It?
- When you want to start multiple tasks together and wait for all of them to finish.
- When you donโt need to cancel other tasks if one fails (unlike
TaskGroup). - When you want to aggregate results from multiple coroutines.
โ๏ธ How Does It Work?
Hereโs a simple example:
Example:
async def main():
results = await asyncio.gather(
fetch_data(1, 2),
fetch_data(2, 1),
fetch_data(3, 3)
)
for result in results:
print(f"Received result: {result}")
asyncio.run(main())
๐ง Explanation:
- All three
fetch_data()coroutines start at the same time. - They finish based on their individual delays.
gather()waits for all of them to complete.- The results are returned in the same order as the coroutines were passed.
โ ๏ธ Notes:
- If one coroutine raises an exception,
gather()will raise it after all tasks finish. - If you want to cancel all tasks when one fails, use
return_exceptions=Trueor considerTaskGroup(Python 3.11+).
๐ asyncio.TaskGroup (Python 3.11+)
- Preferred way to manage multiple tasks.
- Provides built-in error handling.
- Cancels all tasks if any task fails.
๐งฉ asyncio.TaskGroup โ Modern Way to Manage Multiple Tasks
โ What is it?
TaskGroup is a high-level API in asyncio that helps you manage multiple asynchronous tasks as a group. Itโs like a team leader that:
- Starts all tasks together,
- Waits for all of them to finish,
- Cancels the rest if any one fails.
Itโs safer and cleaner than manually creating and tracking tasks with asyncio.create_task().
๐ก Why Use TaskGroup?
- โ Automatic error handling: If one task fails, the rest are cancelled.
- โ Cleaner syntax: No need to manually track or await each task.
- โ Structured concurrency: Tasks are scoped and managed together.
๐ When to Use It?
- When you want to run multiple coroutines concurrently.
- When you want automatic cleanup if something goes wrong.
- When you want to group related tasks under one context.
โ๏ธ How Does It Work?
Hereโs a simple example:
Example:
async def main():
tasks = []
async with asyncio.TaskGroup() as tg:
for i, delay in enumerate([2, 1, 3], start=1):
task = tg.create_task(fetch_data(i, delay))
tasks.append(task)
results = [task.result() for task in tasks]
for result in results:
print(f"Received result: {result}")
asyncio.run(main())
๐ง Explanation:
async with asyncio.TaskGroup()creates a group context.tg.create_task()schedules each coroutine.- If any task raises an exception, the group cancels the rest.
- After the block, all tasks are either completed or cancelled.
โ ๏ธ Notes:
TaskGroupis available only in Python 3.11+.- Itโs part of Pythonโs move toward structured concurrency โ making async code easier to reason about and safer to run.
๐ฎ Futures
- A Future is a low-level awaitable object representing a result that will be available in the future.
- Usually, you donโt create futures manually, but sometimes itโs useful.
๐ฎ asyncio.Future โ A Placeholder for a Result
โ What is a Future?
A Future is a low-level object that represents a result that may not be available yet. Think of it as a promise that a value will be set in the future.
Itโs like ordering food at a restaurant:
- You place the order (create a Future).
- You wait for it to be ready (await the Future).
- The kitchen sets the result (sets the Futureโs value).
- You get your food (the result is available).
๐ก Why Use Futures?
- To manually control when a coroutine gets its result.
- To bridge between callback-based code and
async/await. - To coordinate between coroutines when one needs to wait for another to finish something specific.
๐ When to Use Futures?
- When youโre writing low-level asyncio code.
- When you need to manually trigger the completion of a task.
- When integrating with non-async code or external event sources.
โ๏ธ How Does It Work?
Hereโs a simple example:
Example:
async def set_future_result(future, value):
await asyncio.sleep(1)
future.set_result(value)
async def main():
loop = asyncio.get_running_loop()
future = loop.create_future()
asyncio.create_task(set_future_result(future, "Future result is ready"))
result = await future
print(f"Received future result: {result}")
asyncio.run(main())
๐ง Explanation:
loop.create_future()creates a new Future object.set_future_result()sets the result after a delay.await futurepauses until the result is set.
โ ๏ธ Important Notes:
- You usually donโt need to create Futures manually in high-level asyncio code.
- They are mostly used internally by asyncio or in advanced use cases.
๐ Synchronization Primitives
Lock
- Prevents multiple coroutines from accessing a shared resource simultaneously.
๐ asyncio.Lock โ Ensuring Exclusive Access
โ What is it?
A Lock is a synchronization primitive that ensures only one coroutine can access a shared resource at a time. Itโs like a key to a room โ only one coroutine can hold the key and enter the room, others must wait.
๐ก Why use it?
To prevent race conditions โ situations where multiple coroutines try to modify shared data at the same time, leading to inconsistent or incorrect results.
๐ When to use it?
- When multiple coroutines read/write shared variables.
- When you need to protect critical sections of code.
- When you want to serialize access to a resource.
โ๏ธ How it works?
Example:
import asyncio
shared_resource = 0
lock = asyncio.Lock()
async def modify_resource():
global shared_resource
async with lock:
print(f"Resource before: {shared_resource}")
shared_resource += 1
await asyncio.sleep(1)
print(f"Resource after: {shared_resource}")
async def main():
await asyncio.gather(modify_resource(), modify_resource())
asyncio.run(main())
๐ง Explanation:
- Two coroutines try to modify
shared_resource. - The
async with lock:ensures that only one coroutine enters the critical section at a time. - Without the lock, both coroutines might read the same value and overwrite each otherโs changes.
๐ asyncio.Semaphore โ Controlling Access to Shared Resources
โ What is it?
A Semaphore is a counter that controls access to a shared resource. It allows a fixed number of coroutines to access a resource at the same time.
๐ก Why use it?
When you have a limited number of โslotsโ (e.g., database connections, API rate limits, file handles), a semaphore ensures that no more than a certain number of coroutines access the resource concurrently.
๐ When to use it?
- When you want to limit concurrency.
- When accessing rate-limited APIs.
- When managing resource pools (e.g., DB connections, file handles).
โ๏ธ How it works?
Example:
async def access_resource(semaphore, resource_id):
async with semaphore:
print(f"Accessing resource {resource_id}")
await asyncio.sleep(1)
print(f"Releasing resource {resource_id}")
async def main():
semaphore = asyncio.Semaphore(2) # Allow 2 concurrent accesses
await asyncio.gather(*(access_resource(semaphore, i) for i in range(5)))
asyncio.run(main())
๐ง Explanation: Even though 5 coroutines are created, only 2 run at a time. The rest wait until a slot is free.
๐ฃ asyncio.Event โ Signaling Between Coroutines
โ What is it?
An Event is a simple flag that coroutines can wait for. One coroutine can set the event, and others can wait for it.
๐ก Why use it?
Itโs useful when one coroutine needs to signal others to start or continue execution.
๐ When to use it?
- When you need to coordinate between coroutines.
- When one coroutine depends on another to complete a task or reach a state.
โ๏ธ How it works?
Example:
async def waiter(event):
print("Waiting for event to be set")
await event.wait()
print("Event set, continuing")
async def setter(event):
await asyncio.sleep(2)
event.set()
print("Event has been set")
async def main():
event = asyncio.Event()
await asyncio.gather(waiter(event), setter(event))
asyncio.run(main())
๐ง Explanation: The waiter pauses until the setter sets the event. Once set, all waiting coroutines resume.
๐ Summary: When to Use Asyncio
- Use asyncio for managing many I/O-bound tasks that spend time waiting.
- Use threads for parallel tasks that share data and have minimal CPU use.
- Use processes for CPU-intensive tasks to maximize performance.
๐ Final Notes
- Asyncio is a powerful tool for writing concurrent Python code.
- It uses cooperative multitasking, so coroutines must use
awaitto yield control. - Proper error handling and synchronization are important when working with multiple coroutines.
- Python 3.11 introduced
asyncio.TaskGroupfor better task management.
๋ฉํ๋ฐ์ดํฐ
- post_id
- 49e801a9d2ee
- slug
- python-asyncio-explained-simple-and-clear-guide-49e801a9d2ee
- url
- https://medium.com/@mukeshdani/python-asyncio-explained-simple-and-clear-guide-49e801a9d2ee
- canonical_url
- https://medium.com/@mukeshdani/python-asyncio-explained-simple-and-clear-guide-49e801a9d2ee
- author_url
- https://medium.com/@mukeshdani
- status
- ok
- fetched_at
- 2026-07-19 14:15:38