โ† Back to list

๐ŸŒŸ Python Asyncio Explained: Simple and Clear Guide ๐ŸŒŸ

Introduction

Mukesh Dani ยท 2025-06-05 04:47 ยท 90 claps ยท 7.6 min read
#asyncio #python #coroutine #gather #task-group
Open on Medium โ†—

๐ŸŒŸ 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 await the task later to get its result.

โš ๏ธ Notes:

  • If you donโ€™t await the 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=True or consider TaskGroup (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:

  • TaskGroup is 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 future pauses 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 await to yield control.
  • Proper error handling and synchronization are important when working with multiple coroutines.
  • Python 3.11 introduced asyncio.TaskGroup for 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