asyncio in Python
Sources and guides:
uvloop - fast drop-in replacement of the built-in asyncio event loop in Python.
What is asyncio?
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 docs
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:
Coroutines
they are created when you call an async function. Also known as coroutine function
Tasks
they are wrappers around coroutines that are scheduled on the event loop
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
awaitjust waits for the task to be done before your code continuesThe order you
awaitdoesn'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
asyncio.gather() with CoroutinesCoroutine 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]becomesa, b, cSo
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
asyncio.gather() with TasksDifference from using it with coroutines:
Tasks are created (and start running) before
gather()gather()just waits for them to finishFunctionally equivalent to the above example
When this matters:
asyncio.TaskGroup() (Python 3.11+)
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
awaiteach taskBuilt-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 exitsThe results are already available (tasks finished during
async withexit)
Asyncio networking: sockets vs streams vs protocols
Diagnose slow python code video: https://www.youtube.com/watch?v=m_a0fN48Alw
Last updated