asyncio in Python

Sources and guides:


uvloop - fast drop-in replacement of the built-in asyncio event loop in Python.


What is asyncio?

Docsarrow-up-right

Cheatsheetarrow-up-right

The asyncio library enables asynchronous programming in Python, allowing you to write concurrent code using the async/await syntax. It's ideal for I/O-bound operations like network requests, file operations, or database queries.

Key concepts

  • Event loop: engine that runs and manages asynchronous functions. You can think of it as a scheduler. It keeps track of all out tasks, and when a task is suspended because it's waiting for something else, control returns to the event loop which then finds another task to either start or resume. Event loop docsarrow-up-right

What are awaitable objects?

Awaitables are any objects that implement a special __await__() method under the hood. await pauses a coroutine until the awaited operation completes, allowing other tasks to run in the meantime. await yields control over to the event loop and it's going to suspend the main coroutine until a certain task is complete.

There are 3 main types of awaitable objects in asyncio:

1

Coroutines

they are created when you call an async function. Also known as coroutine function

2

Tasks

they are wrappers around coroutines that are scheduled on the event loop

3

Futures

they are low-level objects representing eventual results

In the example below, there is a pause at asyncio.sleep() which is a primitive designed for pausing the execution of the current coroutine or task without blocking the entire event loop. This allows other tasks or coroutines to run concurrently while the current one is "sleeping." It imitates IO-bound operations. The total execution time will be as long as the longest task in the event loop.

Note: the event loop will be blocked if you try to use time.sleep() which means no other tasks can run during this time

You can wrap coroutines in a task using async.create_task() . It's handed to the event loop and scheduled to run whenever it gets a chance. The task will keep track of whether the coroutine finished successfully, raised and error or got cancelled just like a future would.

In fact, tasks are futures under the hood but with extra logic to actually run the coroutines and do the work. You'll work with tasks and not futures in most of the code. Unlike coroutine objects, tasks can be scheduled on the event loop and just sit there without being run until the loop gets control. And this is the key of asyncio. You can queue up multiple tasks at once and then the event loop will be able to run them whenever it's ready. Letting them take turns while waiting on IO.

Note: when we await something, we're not guaranteeing that it's going to be run immediately at that particular moment. What we are guaranteeing is that we're going to be done with what we awaited before moving on.

When you write await task1, you're saying:

  • "I need the result of task1 before I continue"

  • NOT "Run task1 now"

  • create_task() schedules the task to start running immediately

  • await just waits for the task to be done before your code continues

  • The order you await doesn't control when tasks start—it controls when your code waits for their results

As you've seen above, you can run your event loop by using .run() method, however, there is a lower-level method as well.


asyncio.gather() with Coroutines

Coroutine vs Task:

  • fetch_data(1) creates a coroutine object (not running yet!)

  • asyncio.create_task(fetch_data(1)) creates a Task (starts running immediately)

What gather() does:

  • Takes multiple coroutines or tasks

  • Starts them all concurrently (if they're coroutines)

  • Waits for ALL of them to complete

  • Returns results in the same order as the input (not completion order!)

The * operator:

  • Unpacks the list: *[a, b, c] becomes a, b, c

  • So gather(*coroutines)gather(fetch_data(1), fetch_data(2))

return_exceptions=True:

  • If a task raises an exception, it's returned as a value instead of raising

  • Without this, one exception would cancel all other tasks. But with return_exceptions=True, if one coroutine/task fails, the others are still executed.

  • Example output with exception: ['Result of 1', Exception('error')]


asyncio.gather() with Tasks

Difference from using it with coroutines:

  • Tasks are created (and start running) before gather()

  • gather() just waits for them to finish

  • Functionally equivalent to the above example

When this matters:

asyncio.TaskGroup() (Python 3.11+)

What's different:

Context Manager (async with):

  • Automatically waits for all tasks when exiting the block

  • No need to manually await each task

  • Built-in exception handling: if ANY task fails, all tasks are cancelled

Getting results:

  • Tasks aren't awaited individually

  • Call .result() on each task object after the context manager exits

  • The results are already available (tasks finished during async with exit)



Asyncio networking: sockets vs streams vs protocols



Diagnose slow python code video: https://www.youtube.com/watch?v=m_a0fN48Alwarrow-up-right


Last updated