âčïž Skipped - page is already crawled
| Filter | Status | Condition | Details |
|---|---|---|---|
| HTTP status | PASS | download_http_code = 200 | HTTP 200 |
| Age cutoff | PASS | download_stamp > now() - 6 MONTH | 0 months ago |
| History drop | PASS | isNull(history_drop_reason) | No drop reason |
| Spam/ban | PASS | fh_dont_index != 1 AND ml_spam_score = 0 | ml_spam_score=0 |
| Canonical | PASS | meta_canonical IS NULL OR = '' OR = src_unparsed | Not set |
| Property | Value |
|---|---|
| URL | https://realpython.com/async-io-python/ |
| Last Crawled | 2026-04-16 12:26:25 (21 hours ago) |
| First Indexed | 2019-01-16 15:15:01 (7 years ago) |
| HTTP Status Code | 200 |
| Meta Title | Python's asyncio: A Hands-On Walkthrough â Real Python |
| Meta Description | Explore how Python asyncio works and when to use it. Follow hands-on examples to build efficient programs with coroutines and awaitable tasks. |
| Meta Canonical | null |
| Boilerpipe Text | by
Leodanis Pozo Ramos
Publication date
Jul 30, 2025
Reading time estimate
38m
advanced
python
Pythonâs
asyncio
library enables you to write concurrent code using the
async
and
await
keywords. The core building blocks of async I/O in Python are awaitable objectsâmost often coroutinesâthat an event loop schedules and executes asynchronously. This programming model lets you efficiently manage multiple I/O-bound tasks within a single thread of execution.
In this tutorial, youâll learn how Python
asyncio
works, how to define and run coroutines, and when to use asynchronous programming for better performance in applications that perform I/O-bound tasks.
By the end of this tutorial, youâll understand that:
Pythonâs
asyncio
provides a framework for writing single-threaded
concurrent code
using
coroutines
,
event loops
, and
non-blocking I/O operations
.
For I/O-bound tasks, async I/O
can often outperform multithreading
âespecially when managing a large number of concurrent tasksâbecause it avoids the overhead of thread management.
You should use
asyncio
when your application spends significant time waiting on
I/O operations
, such as network requests or file access, and you want to
run many of these tasks concurrently
without creating extra threads or processes.
Through hands-on examples, youâll gain the practical skills to write efficient Python code using
asyncio
that scales gracefully with increasing I/O demands.
Take the Quiz:
Test your knowledge with our interactive âPython's asyncio: A Hands-On Walkthroughâ quiz. Youâll receive a score upon completion to help you track your learning progress:
A First Look at Async I/O
Before exploring
asyncio
, itâs worth taking a moment to compare async I/O with other concurrency models to see how it fits into Pythonâs broader, sometimes dizzying, landscape. Here are some essential concepts to start with:
Parallelism
consists of executing multiple operations at the same time.
Multiprocessing
is a means of achieving parallelism that entails spreading tasks over a computerâs central processing unit (CPU) cores. Multiprocessing is well-suited for CPU-bound tasks, such as tightly bound
for
loops
and mathematical computations.
Concurrency
is a slightly broader term than parallelism, suggesting that multiple tasks have the ability to run in an overlapping manner. Concurrency doesnât necessarily imply parallelism.
Threading
is a concurrent execution model in which multiple threads take turns executing tasks. A single process can contain multiple threads. Pythonâs relationship with threading is complicated due to the
global interpreter lock (GIL)
, but thatâs beyond the scope of this tutorial.
Threading is good for
I/O-bound tasks
. An I/O-bound job is dominated by a lot of waiting on
input/output (I/O)
to complete, while a
CPU-bound task
is characterized by the computerâs cores continually working hard from start to finish.
The Python
standard library
has offered longstanding
support for these models
through its
multiprocessing
,
concurrent.futures
, and
threading
packages.
Now itâs time to add a new member to the mix. In recent years, a separate model has been more comprehensively built into
CPython
:
asynchronous I/O
, commonly called
async I/O
. This model is enabled through the standard libraryâs
asyncio
package and the
async
and
await
keywords.
The
asyncio
package is billed by the Python documentation as a
library to write concurrent code
. However, async I/O isnât threading or multiprocessing. Itâs not built on top of either of these.
Async I/O is a single-threaded, single-process technique that uses
cooperative multitasking
. Async I/O gives a feeling of concurrency despite using a single thread in a single process.
Coroutines
âor
coro
for shortâare a central feature of async I/O and can be scheduled concurrently, but theyâre not inherently concurrent.
To reiterate, async I/O is a model of concurrent programming, but itâs not parallelism. Itâs more closely aligned with threading than with multiprocessing, but itâs different from both and is a standalone member of the concurrency ecosystem.
That leaves one more term. What does it mean for something to be
asynchronous
? This isnât a rigorous definition, but for the purposes of this tutorial, you can think of two key properties:
Asynchronous routines
can
pause
their execution while waiting for a result and allow other routines to run in the meantime.
Asynchronous code
facilitates the concurrent execution of tasks by coordinating asynchronous routines.
Hereâs a diagram that puts it all together. The white terms represent concepts, and the green terms represent the ways theyâre implemented:
Diagram Comparing Concurrency and Parallelism in Python (Threading, Async I/O, Multiprocessing)
For a thorough exploration of threading versus multiprocessing versus async I/O, pause here and check out the
Speed Up Your Python Program With Concurrency
tutorial. For now, youâll focus on async I/O.
Async I/O Explained
Async I/O may seem counterintuitive and paradoxical at first. How does something that facilitates concurrent code use a single thread in a single CPU core? Miguel Grinbergâs
PyCon
talk explains everything quite beautifully:
Chess master Judit PolgĂĄr hosts a chess exhibition in which she plays multiple amateur players. She has two ways of conducting the exhibition:
synchronously
and
asynchronously
.
Assumptions:
24 opponents
Judit makes each chess move in 5 seconds
Opponents each take 55 seconds to make a move
Games average 30 pair-moves (60 moves total)
Synchronous version
: Judit plays one game at a time, never two at the same time, until the game is complete. Each game takes
(55 + 5) * 30 == 1800
seconds, or 30 minutes. The entire exhibition takes
24 * 30 == 720
minutes, or
12 hours
.
Asynchronous version
: Judit moves from table to table, making one move at each table. She leaves the table and lets the opponent make their next move during the wait time. One move on all 24 games takes Judit
24 * 5 == 120
seconds, or 2 minutes. The entire exhibition is now cut down to
120 * 30 == 3600
seconds, or just
1 hour
. (
Source
)
Thereâs only one Judit PolgĂĄr, who makes only one move at a time. Playing asynchronously cuts the exhibition time down from 12 hours to 1 hour. Async I/O applies this principle to programming. In async I/O, a programâs event loopâmore on that laterâruns multiple tasks, allowing each to take turns running at the optimal time.
Async I/O takes long-running
functions
âlike a complete chess game in the example aboveâthat would block a programâs execution (Judit PolgĂĄrâs time). It manages them in a way so other functions can run during that downtime. In the chess example, Judit PolgĂĄr plays with another participant while the previous ones make their moves.
Async I/O Isnât Simple
Building durable multithreaded code can be challenging and prone to errors. Async I/O avoids some of the potential speed bumps you might encounter with a multithreaded design. However, thatâs not to say that
asynchronous programming
is a simple task in Python.
Be aware that async programming can get tricky when you venture a bit below the surface level. Pythonâs async model is built around concepts such as callbacks, coroutines, events, transports, protocols, and
futures
âeven just the terminology can be intimidating.
That said, the ecosystem around async programming in Python has improved significantly. The
asyncio
package has matured and now provides a stable
API
. Additionally, its documentation has received a considerable overhaul, and some high-quality resources on the subject have also emerged.
Async I/O in Python With
asyncio
Now that you have some background on async I/O as a concurrency model, itâs time to explore Pythonâs implementation. Pythonâs
asyncio
package and its two related keywords,
async
and
await
, serve different purposes but come together to help you declare, build, execute, and manage asynchronous code.
Coroutines and Coroutine Functions
At the heart of async I/O is the concept of a
coroutine
, which is an object that can suspend its execution and resume it later. In the meantime, it can pass the control to an event loop, which can execute another coroutine. Coroutine objects result from calling a
coroutine function
, also known as an
asynchronous function
. You define one with theÂ
async def
 construct.
Before writing your first piece of asynchronous code, consider the following example that runs synchronously:
The
count()
function
prints
One
and waits for a second, then prints
Two
and waits for another second. The loop in the
main()
function executes
count()
three times. Below, in the
if __name__ == "__main__"
condition, you take a snapshot of the current time at the beginning of the execution, call
main()
, compute the total time, and display it on the screen.
When you
run this script
, youâll get the following output:
The script prints
One
and
Two
alternatively, taking a second between each printing operation. In total, it takes a bit more than six seconds to run.
If you update this script to use Pythonâs async I/O model, then it would look something like the following:
Now, you use the
async
keyword to turn
count()
into a coroutine function that prints
One
, waits for one second, then prints
Two
, and waits another second. You use the
await
keyword to
await
the execution of
asyncio.sleep()
. This gives the control back to the programâs event loop, saying:
I will sleep for one second. Go ahead and run something else in the meantime.
The
main()
function is another coroutine function that uses
asyncio.gather()
to run three instances of
count()
concurrently. You use the
asyncio.run()
function to launch the
event loop
and execute
main()
.
Compare the performance of this version to that of the synchronous version:
Thanks to the async I/O approach, the total execution time is just over two seconds instead of six, demonstrating the efficiency of
asyncio
for I/O-bound tasks.
While using
time.sleep()
and
asyncio.sleep()
may seem banal, they serve as stand-ins for time-intensive processes that involve wait time. A call to
time.sleep()
can represent a time-consuming blocking function call, while
asyncio.sleep()
is used to stand in for a
non-blocking call
that also takes some time to complete.
As youâll see in the next section, the benefit of awaiting something, including
asyncio.sleep()
, is that the surrounding function can temporarily cede control to another function thatâs more readily able to do something immediately. In contrast,
time.sleep()
or any other blocking call is incompatible with asynchronous Python code because it stops everything in its tracks for the duration of the sleep time.
The
async
and
await
Keywords
At this point, a more formal definition of
async
,
await
, and the coroutine functions they help you create is in order:
The
async def
syntax construct introduces either a
coroutine function
or an
asynchronous generator
.
The
async with
and
async for
syntax constructs introduce asynchronous
with
statements
and
for
loops
, respectively.
The
await
keyword suspends the execution of the surrounding coroutine and passes control back to the event loop.
To clarify the last point a bit, when Python encounters an
await f()
expression in the scope of a
g()
coroutine,
await
tells the event loop:
suspend the execution of
g()
until the result of
f()
is returned. In the meantime, let something else run.
In code, that last bullet point looks roughly like the following:
Thereâs also a strict set of rules around when and how you can use
async
and
await
. These rules are helpful whether youâre still picking up the syntax or already have exposure to using
async
and
await
:
Using the
async def
construct, you can define a coroutine function. It may use
await
,
return
, or
yield
, but all of these are optional:
await
,
return
, or both can be used in regular coroutine functions. To call a coroutine function, you must either
await
it to get its result or run it directly in an event loop.
yield
used in an
async def
function creates an asynchronous generator. To iterate over this generator, you can use an
async for
loop or a comprehension
.
async def
may not use
yield from
, which will raise a
SyntaxError
.
Using
await
outside of an
async def
function also raises a
SyntaxError
. You can only use
await
in the body of coroutines.
Here are some terse examples that summarize these rules:
Finally, when you use
await f()
, itâs required that
f()
be an object thatâs
awaitable
, which is either another coroutine or an object defining an
.__await__()
special method
that returns an iterator. For most purposes, you should only need to worry about coroutines.
Hereâs a more elaborate example of how async I/O cuts down on wait time. Suppose you have a coroutine function called
make_random()
that keeps producing random integers in the range [0, 10] and returns when one of them exceeds a threshold. In the following example, you run this function asynchronously three times. To differentiate each call, you use colors:
The colorized output speaks louder than a thousand words. Hereâs how this script is carried out:
This program defines the
makerandom()
coroutine and runs it concurrently with three different inputs. Most programs will consist of small, modular coroutines and a wrapper function that serves to
chain
each smaller coroutine. In
main()
, you gather the three tasks. The three calls to
makerandom()
are your
pool of tasks
.
While the random number generation in this example is a CPU-bound task, its impact is negligible. The
asyncio.sleep()
simulates an I/O-bound task and makes the point that only I/O-bound or non-blocking tasks benefit from the async I/O model.
The Async I/O Event Loop
In asynchronous programming, an event loop is like an
infinite loop
that monitors coroutines, takes feedback on whatâs idle, and looks around for things that can be executed in the meantime. Itâs able to wake up an idle coroutine when whatever that coroutine is waiting for becomes available.
The recommended way to start an event loop in modern Python is to use
asyncio.run()
. This function is responsible for getting the event loop, running tasks until they complete, and closing the loop. You canât call this function when another async event loop is running in the same code.
You can also get an instance of the running loop with the
get_running_loop()
function:
If you need to interact with the event loop within a Python program, the above pattern is a good way to do it. The
loop
object supports introspection with
.is_running()
and
.is_closed()
. It can be useful when you want to
schedule a callback
by passing the loop as an argument, for example. Note that
get_running_loop()
raises a
RuntimeError
exception if thereâs no running event loop.
Whatâs more important is understanding what goes on beneath the surface of the event loop. Here are a few points worth stressing:
Coroutines donât do much on their own until theyâre tied to the event loop.
By default, an async event loop runs in a single thread and on a single CPU core. In most
asyncio
applications, there will be only one event loop, typically in the main thread. Running multiple event loops in different threads is technically possible, but not commonly needed or recommended.
Event loops are pluggable. You can write your own implementation and have it run tasks just like the event loops provided in
asyncio
.
Regarding the first point, if you have a coroutine that awaits others, then calling it in isolation has little effect:
In this example, calling
main()
directly returns a coroutine object that you canât use in isolation. You need to use
asyncio.run()
to schedule the
main()
coroutine for execution on the event loop:
You typically wrap your
main()
coroutine in an
asyncio.run()
call. You can execute lower-level coroutines with
await
.
Finally, the fact that the event loop is
pluggable
means that you can use any working implementation of an event loop, and thatâs unrelated to your structure of coroutines. The
asyncio
package ships with two different
event loop implementations
.
The default event loop implementation depends on your platform and Python version. For example, on Unix, the default is typically
SelectorEventLoop
, while Windows uses
ProactorEventLoop
for better subprocess and I/O support.
Third-party event loops are also available. For example, the
uvloop
package provides an alternative implementation that promises to be faster than the
asyncio
loops.
The
asyncio
REPL
Starting with
Python 3.8
, the
asyncio
module includes a specialized interactive shell known as the
asyncio REPL
. This environment allows you to use
await
directly at the top level, without wrapping your code in a call to
asyncio.run()
. This tool facilitates experimenting, debugging, and learning about
asyncio
in Python.
To start the
REPL
, you can run the following command:
Once you get the
>>>
prompt, you can start running asynchronous code there. Consider the example below, where you reuse the code from the previous section:
This example works the same as the one in the previous section. However, instead of running
main()
using
asyncio.run()
, you use
await
directly.
Common Async I/O Programming Patterns
Async I/O has its own set of possible programming patterns that allow you to write better asynchronous code. In practice, you can
chain coroutines
or use a
queue
of coroutines. Youâll learn how to use these two patterns in the following sections.
Coroutine Chaining
A key feature of coroutines is that you can
chain
them together. Remember, a coroutine is awaitable, so another coroutine can await it using the
await
keyword. This makes it easier to break your program into smaller, manageable, and reusable coroutines.
The example below simulates a two-step process that fetches information about a user. The first step fetches the user information, and the second step fetches their published posts:
In this example, you define two major coroutines:
fetch_user()
and
fetch_posts()
. Both simulate a network call with a random delay using
asyncio.sleep()
.
In the
fetch_user()
coroutine, you return a mock user
dictionary
. In
fetch_posts()
, you use that dictionary to return a list of mock posts attributed to the user at hand. Random delays simulate real-world asynchronous behavior like network latency.
The coroutine chaining happens in the
get_user_with_posts()
. This coroutine awaits
fetch_user()
and stores the result in the
user
variable
. Once the user information is available, itâs passed to
fetch_posts()
to retrieve the posts asynchronously.
In
main()
, you use
asyncio.gather()
to run the chained coroutines by executing
get_user_with_posts()
as many times as the number of user IDs you have.
Hereâs the result of executing the script:
If you sum up the time of all the operations, then this example would take around 7.6 seconds with a synchronous implementation. However, with the asynchronous implementation, it only takes 2.68 seconds.
The pattern, consisting of awaiting one coroutine and passing its result into the next, creates a
coroutine chain
, where each step depends on the previous one. This example mimics a common async workflow where you get one piece of information and use it to get related data.
Coroutine and Queue Integration
The
asyncio
package provides a few
queue-like classes
that are designed to be similar to
classes
in the
queue
module. In the examples so far, you havenât needed a queue structure. In
chained.py
, each task is performed by a coroutine, which you chain with others to pass data from one to the next.
An alternative approach is to use
producers
that add items to a
queue
. Each producer may add multiple items to the queue at staggered, random, and unannounced times. Then, a group of
consumers
pulls items from the queue as they show up, greedily and without waiting for any other signal.
In this design, thereâs no chaining between producers and consumers. Consumers donât know the number of producers, and vice versa.
It takes an individual producer or consumer a variable amount of time to add and remove items from the queue. The queue serves as a throughput that can communicate with the producers and consumers without them talking to each other directly.
A queue-based version of
chained.py
is shown below:
In this example, the
producer()
function asynchronously fetches mock user data. Each fetched user dictionary is placed into an
asyncio.Queue
object, which shares the data with consumers. After producing all user objects, the producer inserts a
sentinel value
âalso known as a
poison pill
in this contextâfor each consumer to signal that no more data will be sent, allowing the consumers to shut down cleanly.
The
consumer()
function continuously reads from the queue. If it receives a user dictionary, it simulates fetching that userâs posts, waits a random delay, and prints the results. If it gets the sentinel value, then it breaks the loop and exits.
This decoupling allows multiple consumers to process users concurrently, even while the producer is still generating users, and the queue ensures safe and ordered communication between producers and consumers.
The queue is the communication point between the producers and consumers, enabling a scalable and responsive system.
Hereâs how the code works in practice:
Again, the code runs in only 2.68 seconds, which is more efficient than a synchronous solution. The result is pretty much the same as when you used chained coroutines in the previous section.
Other Async I/O Features in Python
Pythonâs async I/O features extend beyond the
async def
and
await
constructs. They include other advanced tools that make asynchronous programming more expressive and consistent with regular Python constructs.
In the following sections, youâll explore powerful async features, including async loops and comprehensions, the
async with
statement, and exception groups. These features will help you write cleaner, more readable asynchronous code.
Async Iterators, Loops, and Comprehensions
Apart from using
async
and
await
to create coroutines, Python also provides the
async for
construct to iterate over an
asynchronous iterator
. An asynchronous iterator allows you to iterate over asynchronously generated data. While the loop runs, it gives control back to the event loop so that other async tasks can run.
A natural extension of this concept is an
asynchronous generator
. Hereâs an example that generates powers of two and uses them in a loop and comprehension:
Thereâs a crucial distinction between synchronous and asynchronous generators, loops, and comprehensions. Their asynchronous counterparts donât inherently make iteration concurrent. Instead, they allow the event loop to run other tasks between iterations when you explicitly yield control by using
await
. The iteration itself is still sequential unless you introduce concurrency by using
asyncio.gather()
.
Using
async for
and
async with
is only required when working with asynchronous iterators or context managers, where a regular
for
or
with
would raise errors.
Async
with
Statements
The
with
statement
also has an
asynchronous
version,
async with
. This construct is quite common in async code, as many
I/O-bound tasks
involve setup and teardown phases.
For example, say you need to write a coroutine to check whether some websites are online. To do that, you can use
aiohttp
, which is a third-party library that you need to install by running
python -m pip install aiohttp
on your command line.
Hereâs a quick example that implements the required functionality:
In this example, you use
aiohttp
and
asyncio
to perform concurrent
HTTP GET
requests to a list of websites. The
check()
coroutine fetches and prints the websiteâs status. The
async with
statement ensures that both
ClientSession
and the individual HTTP response are properly and asynchronously managed by opening and closing them without blocking the event loop.
In this example, using
async with
guarantees that the underlying network resources, including connections and sockets, are correctly released, even if an error occurs.
Finally,
main()
runs the
check()
coroutines concurrently, allowing you to fetch the URLs in parallel without waiting for one to finish before starting the next.
Other
asyncio
Tools
In addition to
asyncio.run()
, youâve used a few other package-level functions, such as
asyncio.gather()
and
asyncio.get_event_loop()
. You can use
asyncio.create_task()
to schedule the execution of a coroutine object, followed by the usual call to the
asyncio.run()
function:
This pattern includes a subtle detail you need to be aware of: if you create tasks with
create_task()
but donât await them or wrap them in
gather()
, and your
main()
coroutine finishes, then those manually created tasks will be canceled when the event loop ends. You must await all tasks you want to complete.
The
create_task()
function wraps an awaitable object into a higher-level
Task
object thatâs scheduled to run concurrently on the event loop in the background. In contrast, awaiting a coroutine runs it immediately, pausing the execution of the caller until the awaited coroutine finishes.
The
gather()
function is meant to neatly put a collection of coroutines into a single
future object
. This object represents a placeholder for a result thatâs initially unknown but will be available at some point, typically as the result of asynchronous computations.
If you await
gather()
and specify multiple tasks or coroutines, then the loop will wait for all the tasks to complete. The result of
gather()
will be a list of the results across the inputs:
You probably noticed that
gather()
waits for the entire result of the whole set of coroutines that you pass it. The order of results from
gather()
is deterministic and corresponds to the order of awaitables originally passed to it.
Alternatively, you can loop over
asyncio.as_completed()
to get tasks as they complete. The function returns a synchronous iterator that yields tasks as they finish. Below, the result of
coro([3, 2, 1])
will be available before
coro([10, 5, 2])
is complete, which wasnât the case with the
gather()
function:
In this example, the
main()
function uses
asyncio.as_completed()
, which yields tasks in the order they complete, not in the order they were started. As the program loops through the tasks, it awaits them, allowing the results to be available immediately upon completion.
As a result, the faster task (
task1
) finishes first and its result is printed earlier, while the longer task (
task2
) completes and prints afterward. The
as_completed()
function is useful when you need to handle tasks dynamically as they finish, which improves responsiveness in concurrent workflows.
Async Exception Handling
Starting with
Python 3.11
, you can use the
ExceptionGroup
class to handle multiple unrelated exceptions that may occur concurrently. This is especially useful when running multiple coroutines that can raise different exceptions. Additionally, the new
except*
syntax helps you gracefully deal with several errors at once.
Hereâs a quick demo of how to use this class in asynchronous code:
In this example, you have three coroutines that raise three different types of
exceptions
. In the
main()
function, you call
gather()
with the coroutines as arguments. You also set the
return_exceptions
argument to
True
so that you can grab the exceptions if they occur.
Next, you use a list comprehension to store the exceptions in a new list. If the list contains at least one exception, then you create an
ExceptionGroup
for them.
To handle this exception group, you can use the following code:
In this code, you wrap the call to
asyncio.run()
in a
try
block. Then, you use the
except*
syntax to catch the expected exception separately. In each case, you print an error message to the screen.
Async I/O in Context
Now that youâve seen a healthy dose of asynchronous code, take a moment to step back and consider when async I/O is the ideal choiceâand how to evaluate whether itâs the right fit or if another concurrency model might be better.
When to Use Async I/O
Using
async def
for functions that perform blocking operationsâsuch as standard file I/O or synchronous network requestsâwill block the entire event loop, negate the benefits of async I/O, and potentially reduce your programâs efficiency. Only use
async def
functions for
non-blocking operations
.
The battle between async I/O and multiprocessing isnât a real battle. You can use both models
in concert
if you want. In practice, multiprocessing should be the right choice if you have multiple CPU-bound tasks.
The contest between async I/O and threading is more direct. Threading isnât simple, and even in cases where threading seems easy to implement, it can still lead to hard-to-trace bugs due to
race conditions
and memory usage, among other things.
Threading also tends to scale less elegantly than async I/O because threads are a system resource with a finite availability. Creating thousands of threads will fail on many machines or can slow down your code. In contrast, creating thousands of async I/O tasks is completely feasible.
Async I/O shines when you have multiple I/O-bound tasks that would otherwise be dominated by blocking wait time, such as:
Network I/O
, whether your program is acting as the server or the client
Serverless designs
, such as a peer-to-peer, multi-user network like a group chat
Read/write operations
where you want to mimic a
fire-and-forget
style approach without worrying about holding a lock on the resource
The biggest reason not to use async I/O is that
await
only supports a specific set of objects that define a particular set of methods. For example, if you want to do async read operations on a certain
database management system (DBMS)
, then youâll need to find a Python wrapper for that DBMS that supports the
async
and
await
syntax.
Libraries Supporting Async I/O
Youâll find several high-quality third-party libraries and frameworks that support or are built on top of
asyncio
in Python, including tools for web servers, databases, networking, testing, and more. Here are some of the most notable:
Web frameworks:
FastAPI
: Modern async web framework for building
web APIs
.
Starlette
: Lightweight
asynchronous server gateway interface (ASGI)
framework for building high-performance async web apps.
Sanic
: Async web framework built for speed using
asyncio
.
Quart
: Async web microframework with the same API as
Flask
.
Tornado
: Performant web framework and asynchronous networking library.
ASGI servers:
uvicorn
: Fast ASGI web server.
Hypercorn
: ASGI server supporting several protocols and configuration options.
Networking tools:
aiohttp
: HTTP client and server implementation using
asyncio
.
HTTPX
: Fully featured async and sync HTTP client.
websockets
: Library for building WebSocket servers and clients with
asyncio
.
aiosmtplib
: Async SMTP client for
sending emails
.
Database tools:
Databases
: Async database access layer compatible with
SQLAlchemy
core.
Tortoise ORM
: Lightweight async object-relational mapper (ORM).
Gino
: Async ORM built on SQLAlchemy core for
PostgreSQL
.
Motor
: Async
MongoDB
driver built on
asyncio
.
Utility libraries:
aiofiles
: Wraps Pythonâs file API for use with
async
and
await
.
aiocache
: Async caching library supporting
Redis
and Memcached.
APScheduler
: A task scheduler with support for async jobs.
pytest-asyncio
: Adds support for testing async functions using
pytest
.
These libraries and frameworks help you write performant async Python applications. Whether youâre building a web server, fetching data over the network, or accessing a database,
asyncio
tools like these give you the power to handle many tasks concurrently with minimal overhead.
Conclusion
Youâve gained a solid understanding of Pythonâs
asyncio
library and the
async
and
await
syntax, learning how asynchronous programming enables efficient management of multiple I/O-bound tasks within a single thread.
Along the way, you explored the differences between concurrency, parallelism, threading, multiprocessing, and asynchronous I/O. You also worked through practical examples using coroutines, event loops, chaining, and queue-based concurrency. On top of that, you learned about advanced
asyncio
features, including async context managers, async iterators, comprehensions, and how to leverage third-party async libraries.
Mastering
asyncio
is essential when building scalable network servers, web APIs, or applications that perform many simultaneous I/O-bound operations.
In this tutorial, youâve learned how to:
Distinguish
between concurrency models and identify when to use
asyncio
for I/O-bound tasks
Write
,
run
, and
chain coroutines
using
async def
and
await
Manage the event loop
and schedule multiple tasks with
asyncio.run()
,
gather()
, and
create_task()
Implement async patterns like
coroutine chaining
and
async queues
for producerâconsumer workflows
Use advanced async features
such as
async for
and
async with
, and integrate with
third-party async libraries
With these skills, youâre ready to build high-performance, modern Python applications that can handle many operations asynchronously.
Frequently Asked Questions
Now that you have some experience with
asyncio
in Python, you can use the questions and answers below to check your understanding and recap what youâve learned.
These FAQs are related to the most important concepts youâve covered in this tutorial. Click the
Show/Hide
toggle beside each question to reveal the answer.
You use
asyncio
to write concurrent code with the
async
and
await
keywords, allowing you to efficiently manage multiple I/O-bound tasks in a single thread without blocking your program.
You typically get better performance from
asyncio
for I/O-bound work because it avoids the overhead and complexity of threads. This allows thousands of tasks to run concurrently without the limitations of Pythonâs GIL.
Use
asyncio
when your program spends a significant amount of time waiting on I/O-bound operationsâsuch as network requests or file accessâand you want to run many of these tasks concurrently and efficiently.
You define a coroutine using the
async def
syntax. To run it, either pass it to
asyncio.run()
or schedule it as a task with
asyncio.create_task()
.
You rely on the event loop to manage the scheduling and execution of your coroutines, giving each one a chance to run whenever it awaits or completes an I/O-bound operation.
Take the Quiz:
Test your knowledge with our interactive âPython's asyncio: A Hands-On Walkthroughâ quiz. Youâll receive a score upon completion to help you track your learning progress: |
| Markdown | [](https://realpython.com/)
- [Start Here](https://realpython.com/start-here/)
- [Learn Python](https://realpython.com/async-io-python/)
[Python Tutorials â In-depth articles and video courses](https://realpython.com/search?kind=article&kind=course&order=newest)
[Learning Paths â Guided study plans for accelerated learning](https://realpython.com/learning-paths/)
[Quizzes & Exercises â Check your learning progress](https://realpython.com/quizzes/)
[Browse Topics â Focus on a specific area or skill level](https://realpython.com/tutorials/all/)
[Community Chat â Learn with other Pythonistas](https://realpython.com/community/)
[Office Hours â Live Q\&A calls with Python experts](https://realpython.com/office-hours/)
[Live Courses â Live, instructor-led Python courses](https://realpython.com/live/)
[Podcast â Hear whatâs new in the world of Python](https://realpython.com/podcasts/rpp/)
[Books â Round out your knowledge and learn offline](https://realpython.com/products/books/)
[Reference â Concise definitions for common Python terms](https://realpython.com/ref/)
[Code Mentor âBeta Personalized code assistance & learning tools](https://realpython.com/mentor/)
[Unlock All Content â](https://realpython.com/account/join/)
- [More](https://realpython.com/async-io-python/)
[Learner Stories](https://realpython.com/learner-stories/) [Python Newsletter](https://realpython.com/newsletter/) [Python Job Board](https://www.pythonjobshq.com/) [Meet the Team](https://realpython.com/team/) [Become a Contributor](https://realpython.com/jobs/)
- [Search](https://realpython.com/search "Search")
- [Join](https://realpython.com/account/join/)
- [SignâIn](https://realpython.com/account/login/?next=%2Fasync-io-python%2F)
[Browse Topics](https://realpython.com/tutorials/all/)
[Guided Learning Paths](https://realpython.com/learning-paths/)
[Basics](https://realpython.com/search?level=basics)
[Intermediate](https://realpython.com/search?level=intermediate)
[Advanced](https://realpython.com/search?level=advanced)
***
[ai](https://realpython.com/tutorials/ai/) [algorithms](https://realpython.com/tutorials/algorithms/) [api](https://realpython.com/tutorials/api/) [best-practices](https://realpython.com/tutorials/best-practices/) [career](https://realpython.com/tutorials/career/) [community](https://realpython.com/tutorials/community/) [databases](https://realpython.com/tutorials/databases/) [data-science](https://realpython.com/tutorials/data-science/) [data-structures](https://realpython.com/tutorials/data-structures/) [data-viz](https://realpython.com/tutorials/data-viz/) [devops](https://realpython.com/tutorials/devops/) [django](https://realpython.com/tutorials/django/) [docker](https://realpython.com/tutorials/docker/) [editors](https://realpython.com/tutorials/editors/) [flask](https://realpython.com/tutorials/flask/) [front-end](https://realpython.com/tutorials/front-end/) [gamedev](https://realpython.com/tutorials/gamedev/) [gui](https://realpython.com/tutorials/gui/) [machine-learning](https://realpython.com/tutorials/machine-learning/) [news](https://realpython.com/tutorials/news/) [numpy](https://realpython.com/tutorials/numpy/) [projects](https://realpython.com/tutorials/projects/) [python](https://realpython.com/tutorials/python/) [stdlib](https://realpython.com/tutorials/stdlib/) [testing](https://realpython.com/tutorials/testing/) [tools](https://realpython.com/tutorials/tools/) [web-dev](https://realpython.com/tutorials/web-dev/) [web-scraping](https://realpython.com/tutorials/web-scraping/)
[Table of Contents](https://realpython.com/async-io-python/#toc)
- [A First Look at Async I/O](https://realpython.com/async-io-python/#a-first-look-at-async-io)
- [Async I/O Explained](https://realpython.com/async-io-python/#async-io-explained)
- [Async I/O Isnât Simple](https://realpython.com/async-io-python/#async-io-isnt-simple)
- [Async I/O in Python With asyncio](https://realpython.com/async-io-python/#async-io-in-python-with-asyncio)
- [Coroutines and Coroutine Functions](https://realpython.com/async-io-python/#coroutines-and-coroutine-functions)
- [The async and await Keywords](https://realpython.com/async-io-python/#the-async-and-await-keywords)
- [The Async I/O Event Loop](https://realpython.com/async-io-python/#the-async-io-event-loop)
- [The asyncio REPL](https://realpython.com/async-io-python/#the-asyncio-repl)
- [Common Async I/O Programming Patterns](https://realpython.com/async-io-python/#common-async-io-programming-patterns)
- [Coroutine Chaining](https://realpython.com/async-io-python/#coroutine-chaining)
- [Coroutine and Queue Integration](https://realpython.com/async-io-python/#coroutine-and-queue-integration)
- [Other Async I/O Features in Python](https://realpython.com/async-io-python/#other-async-io-features-in-python)
- [Async Iterators, Loops, and Comprehensions](https://realpython.com/async-io-python/#async-iterators-loops-and-comprehensions)
- [Async with Statements](https://realpython.com/async-io-python/#async-with-statements)
- [Other asyncio Tools](https://realpython.com/async-io-python/#other-asyncio-tools)
- [Async Exception Handling](https://realpython.com/async-io-python/#async-exception-handling)
- [Async I/O in Context](https://realpython.com/async-io-python/#async-io-in-context)
- [When to Use Async I/O](https://realpython.com/async-io-python/#when-to-use-async-io)
- [Libraries Supporting Async I/O](https://realpython.com/async-io-python/#libraries-supporting-async-io)
- [Conclusion](https://realpython.com/async-io-python/#conclusion)
- [Frequently Asked Questions](https://realpython.com/async-io-python/#frequently-asked-questions)
Mark as Completed
Share
Recommended Course
[ Hands-On Python 3 Concurrency With the asyncio Module 1h 48m · 18 lessons](https://realpython.com/courses/python-3-concurrency-asyncio-module/)

# Python's asyncio: A Hands-On Walkthrough
by [Leodanis Pozo Ramos](https://realpython.com/async-io-python/#author)
Publication date
Jul 30, 2025
Reading time estimate
38m
[68 Comments](https://realpython.com/async-io-python/#reader-comments)
[advanced](https://realpython.com/tutorials/advanced/) [python](https://realpython.com/tutorials/python/)
Mark as Completed
Share
Table of Contents
- [A First Look at Async I/O](https://realpython.com/async-io-python/#a-first-look-at-async-io)
- [Async I/O Explained](https://realpython.com/async-io-python/#async-io-explained)
- [Async I/O Isnât Simple](https://realpython.com/async-io-python/#async-io-isnt-simple)
- [Async I/O in Python With asyncio](https://realpython.com/async-io-python/#async-io-in-python-with-asyncio)
- [Coroutines and Coroutine Functions](https://realpython.com/async-io-python/#coroutines-and-coroutine-functions)
- [The async and await Keywords](https://realpython.com/async-io-python/#the-async-and-await-keywords)
- [The Async I/O Event Loop](https://realpython.com/async-io-python/#the-async-io-event-loop)
- [The asyncio REPL](https://realpython.com/async-io-python/#the-asyncio-repl)
- [Common Async I/O Programming Patterns](https://realpython.com/async-io-python/#common-async-io-programming-patterns)
- [Coroutine Chaining](https://realpython.com/async-io-python/#coroutine-chaining)
- [Coroutine and Queue Integration](https://realpython.com/async-io-python/#coroutine-and-queue-integration)
- [Other Async I/O Features in Python](https://realpython.com/async-io-python/#other-async-io-features-in-python)
- [Async Iterators, Loops, and Comprehensions](https://realpython.com/async-io-python/#async-iterators-loops-and-comprehensions)
- [Async with Statements](https://realpython.com/async-io-python/#async-with-statements)
- [Other asyncio Tools](https://realpython.com/async-io-python/#other-asyncio-tools)
- [Async Exception Handling](https://realpython.com/async-io-python/#async-exception-handling)
- [Async I/O in Context](https://realpython.com/async-io-python/#async-io-in-context)
- [When to Use Async I/O](https://realpython.com/async-io-python/#when-to-use-async-io)
- [Libraries Supporting Async I/O](https://realpython.com/async-io-python/#libraries-supporting-async-io)
- [Conclusion](https://realpython.com/async-io-python/#conclusion)
- [Frequently Asked Questions](https://realpython.com/async-io-python/#frequently-asked-questions)
[Remove ads](https://realpython.com/account/join/)
Recommended Course
[Hands-On Python 3 Concurrency With the asyncio Module](https://realpython.com/courses/python-3-concurrency-asyncio-module/) (1h 48m)
Pythonâs `asyncio` library enables you to write concurrent code using the `async` and `await` keywords. The core building blocks of async I/O in Python are awaitable objectsâmost often coroutinesâthat an event loop schedules and executes asynchronously. This programming model lets you efficiently manage multiple I/O-bound tasks within a single thread of execution.
In this tutorial, youâll learn how Python `asyncio` works, how to define and run coroutines, and when to use asynchronous programming for better performance in applications that perform I/O-bound tasks.
**By the end of this tutorial, youâll understand that:**
- Pythonâs **`asyncio`** provides a framework for writing single-threaded **concurrent code** using **coroutines**, **event loops**, and **non-blocking I/O operations**.
- For I/O-bound tasks, async I/O **can often outperform multithreading**âespecially when managing a large number of concurrent tasksâbecause it avoids the overhead of thread management.
- You should use `asyncio` when your application spends significant time waiting on **I/O operations**, such as network requests or file access, and you want to **run many of these tasks concurrently** without creating extra threads or processes.
Through hands-on examples, youâll gain the practical skills to write efficient Python code using `asyncio` that scales gracefully with increasing I/O demands.
**Get Your Code:** [Click here to download the free sample code](https://realpython.com/bonus/async-io-python-code/) that youâll use to learn about async I/O in Python.
***Take the Quiz:*** Test your knowledge with our interactive âPython's asyncio: A Hands-On Walkthroughâ quiz. Youâll receive a score upon completion to help you track your learning progress:
***
[](https://realpython.com/quizzes/async-io-python/)
**Interactive Quiz**
[Python's asyncio: A Hands-On Walkthrough](https://realpython.com/quizzes/async-io-python/)
Test your knowledge of \`asyncio\` concurrency with this quiz that covers coroutines, event loops, and efficient I/O-bound task management.
## A First Look at Async I/O
Before exploring `asyncio`, itâs worth taking a moment to compare async I/O with other concurrency models to see how it fits into Pythonâs broader, sometimes dizzying, landscape. Here are some essential concepts to start with:
- **Parallelism** consists of executing multiple operations at the same time.
- **Multiprocessing** is a means of achieving parallelism that entails spreading tasks over a computerâs central processing unit (CPU) cores. Multiprocessing is well-suited for CPU-bound tasks, such as tightly bound [`for` loops](https://realpython.com/python-for-loop/) and mathematical computations.
- **Concurrency** is a slightly broader term than parallelism, suggesting that multiple tasks have the ability to run in an overlapping manner. Concurrency doesnât necessarily imply parallelism.
- **Threading** is a concurrent execution model in which multiple threads take turns executing tasks. A single process can contain multiple threads. Pythonâs relationship with threading is complicated due to the [global interpreter lock (GIL)](https://realpython.com/python-gil/), but thatâs beyond the scope of this tutorial.
Threading is good for [**I/O-bound tasks**](https://realpython.com/ref/glossary/io-bound-task/). An I/O-bound job is dominated by a lot of waiting on [**input/output (I/O)**](https://realpython.com/ref/glossary/input-output/) to complete, while a [CPU-bound task](https://realpython.com/ref/glossary/cpu-bound-task/) is characterized by the computerâs cores continually working hard from start to finish.
The Python [standard library](https://realpython.com/ref/glossary/standard-library/) has offered longstanding [support for these models](https://docs.python.org/3/library/concurrency.html) through its `multiprocessing`, `concurrent.futures`, and `threading` packages.
Now itâs time to add a new member to the mix. In recent years, a separate model has been more comprehensively built into [CPython](https://realpython.com/cpython-source-code-guide/): **asynchronous I/O**, commonly called **async I/O**. This model is enabled through the standard libraryâs [**`asyncio`**](https://realpython.com/ref/stdlib/asyncio/) package and the [`async`](https://realpython.com/python-keywords/#the-async-keyword) and [`await`](https://realpython.com/python-keywords/#the-await-keyword) keywords.
**Note:** Async I/O isnât a new concept. It exists inâor is being built intoâother languages such as [Go](https://gobyexample.com/goroutines), [C\#](https://docs.microsoft.com/en-us/dotnet/csharp/async), and [Rust](https://doc.rust-lang.org/book/ch17-00-async-await.html).
The `asyncio` package is billed by the Python documentation as a [library to write concurrent code](https://docs.python.org/3/library/asyncio.html). However, async I/O isnât threading or multiprocessing. Itâs not built on top of either of these.
Async I/O is a single-threaded, single-process technique that uses [cooperative multitasking](https://en.wikipedia.org/wiki/Cooperative_multitasking). Async I/O gives a feeling of concurrency despite using a single thread in a single process. [Coroutines](https://realpython.com/ref/glossary/coroutine/)âor **coro** for shortâare a central feature of async I/O and can be scheduled concurrently, but theyâre not inherently concurrent.
To reiterate, async I/O is a model of concurrent programming, but itâs not parallelism. Itâs more closely aligned with threading than with multiprocessing, but itâs different from both and is a standalone member of the concurrency ecosystem.
That leaves one more term. What does it mean for something to be **asynchronous**? This isnât a rigorous definition, but for the purposes of this tutorial, you can think of two key properties:
1. **Asynchronous routines** can *pause* their execution while waiting for a result and allow other routines to run in the meantime.
2. **Asynchronous code** facilitates the concurrent execution of tasks by coordinating asynchronous routines.
Hereâs a diagram that puts it all together. The white terms represent concepts, and the green terms represent the ways theyâre implemented:
[](https://files.realpython.com/media/Screen_Shot_2018-10-17_at_3.18.44_PM.c02792872031.jpg)
Diagram Comparing Concurrency and Parallelism in Python (Threading, Async I/O, Multiprocessing)
For a thorough exploration of threading versus multiprocessing versus async I/O, pause here and check out the [Speed Up Your Python Program With Concurrency](https://realpython.com/python-concurrency/) tutorial. For now, youâll focus on async I/O.
[Remove ads](https://realpython.com/account/join/)
### Async I/O Explained
Async I/O may seem counterintuitive and paradoxical at first. How does something that facilitates concurrent code use a single thread in a single CPU core? Miguel Grinbergâs [PyCon](https://realpython.com/pycon-guide/) talk explains everything quite beautifully:
> Chess master Judit PolgĂĄr hosts a chess exhibition in which she plays multiple amateur players. She has two ways of conducting the exhibition: *synchronously* and *asynchronously*.
>
> Assumptions:
>
> - 24 opponents
> - Judit makes each chess move in 5 seconds
> - Opponents each take 55 seconds to make a move
> - Games average 30 pair-moves (60 moves total)
>
> **Synchronous version**: Judit plays one game at a time, never two at the same time, until the game is complete. Each game takes *(55 + 5) \* 30 == 1800* seconds, or 30 minutes. The entire exhibition takes *24 \* 30 == 720* minutes, or **12 hours**.
>
> **Asynchronous version**: Judit moves from table to table, making one move at each table. She leaves the table and lets the opponent make their next move during the wait time. One move on all 24 games takes Judit *24 \* 5 == 120* seconds, or 2 minutes. The entire exhibition is now cut down to *120 \* 30 == 3600* seconds, or just **1 hour**. ([Source](https://youtu.be/iG6fr81xHKA?t=4m29s))
Thereâs only one Judit PolgĂĄr, who makes only one move at a time. Playing asynchronously cuts the exhibition time down from 12 hours to 1 hour. Async I/O applies this principle to programming. In async I/O, a programâs event loopâmore on that laterâruns multiple tasks, allowing each to take turns running at the optimal time.
Async I/O takes long-running [functions](https://realpython.com/defining-your-own-python-function/)âlike a complete chess game in the example aboveâthat would block a programâs execution (Judit PolgĂĄrâs time). It manages them in a way so other functions can run during that downtime. In the chess example, Judit PolgĂĄr plays with another participant while the previous ones make their moves.
### Async I/O Isnât Simple
Building durable multithreaded code can be challenging and prone to errors. Async I/O avoids some of the potential speed bumps you might encounter with a multithreaded design. However, thatâs not to say that [asynchronous programming](https://realpython.com/ref/glossary/asynchronous-programming/) is a simple task in Python.
Be aware that async programming can get tricky when you venture a bit below the surface level. Pythonâs async model is built around concepts such as callbacks, coroutines, events, transports, protocols, and [futures](https://docs.python.org/3/library/asyncio-future.html#asyncio.Future)âeven just the terminology can be intimidating.
That said, the ecosystem around async programming in Python has improved significantly. The `asyncio` package has matured and now provides a stable [API](https://realpython.com/ref/glossary/api/). Additionally, its documentation has received a considerable overhaul, and some high-quality resources on the subject have also emerged.
## Async I/O in Python With `asyncio`
Now that you have some background on async I/O as a concurrency model, itâs time to explore Pythonâs implementation. Pythonâs `asyncio` package and its two related keywords, [`async`](https://realpython.com/python-keywords/#the-async-keyword) and [`await`](https://realpython.com/python-keywords/#the-await-keyword), serve different purposes but come together to help you declare, build, execute, and manage asynchronous code.
### Coroutines and Coroutine Functions
At the heart of async I/O is the concept of a [**coroutine**](https://realpython.com/ref/glossary/coroutine/), which is an object that can suspend its execution and resume it later. In the meantime, it can pass the control to an event loop, which can execute another coroutine. Coroutine objects result from calling a [**coroutine function**](https://realpython.com/ref/glossary/coroutine-function/), also known as an **asynchronous function**. You define one with the `async def` construct.
Before writing your first piece of asynchronous code, consider the following example that runs synchronously:
Python `countsync.py`
```
```
The `count()` function [prints](https://realpython.com/python-print/) `One` and waits for a second, then prints `Two` and waits for another second. The loop in the [`main()`](https://realpython.com/python-main-function/) function executes `count()` three times. Below, in the [`if __name__ == "__main__"`](https://realpython.com/if-name-main-python/) condition, you take a snapshot of the current time at the beginning of the execution, call `main()`, compute the total time, and display it on the screen.
When you [run this script](https://realpython.com/run-python-scripts/), youâll get the following output:
Shell
```
```
The script prints `One` and `Two` alternatively, taking a second between each printing operation. In total, it takes a bit more than six seconds to run.
If you update this script to use Pythonâs async I/O model, then it would look something like the following:
Python `countasync.py`
```
```
Now, you use the `async` keyword to turn `count()` into a coroutine function that prints `One`, waits for one second, then prints `Two`, and waits another second. You use the `await` keyword to *await* the execution of `asyncio.sleep()`. This gives the control back to the programâs event loop, saying: *I will sleep for one second. Go ahead and run something else in the meantime.*
The `main()` function is another coroutine function that uses [`asyncio.gather()`](https://realpython.com/async-io-python/#other-asyncio-tools) to run three instances of `count()` concurrently. You use the `asyncio.run()` function to launch the [event loop](https://realpython.com/async-io-python/#the-async-io-event-loop) and execute `main()`.
Compare the performance of this version to that of the synchronous version:
Shell
```
```
Thanks to the async I/O approach, the total execution time is just over two seconds instead of six, demonstrating the efficiency of `asyncio` for I/O-bound tasks.
While using `time.sleep()` and `asyncio.sleep()` may seem banal, they serve as stand-ins for time-intensive processes that involve wait time. A call to `time.sleep()` can represent a time-consuming blocking function call, while `asyncio.sleep()` is used to stand in for a [non-blocking call](https://realpython.com/ref/glossary/non-blocking-operation/) that also takes some time to complete.
As youâll see in the next section, the benefit of awaiting something, including `asyncio.sleep()`, is that the surrounding function can temporarily cede control to another function thatâs more readily able to do something immediately. In contrast, `time.sleep()` or any other blocking call is incompatible with asynchronous Python code because it stops everything in its tracks for the duration of the sleep time.
[Remove ads](https://realpython.com/account/join/)
### The `async` and `await` Keywords
At this point, a more formal definition of `async`, `await`, and the coroutine functions they help you create is in order:
- The **`async def`** syntax construct introduces either a **coroutine function** or an [**asynchronous generator**](https://realpython.com/ref/glossary/asynchronous-generator/).
- The **`async with`** and **`async for`** syntax constructs introduce asynchronous **`with` statements** and **`for` loops**, respectively.
- The **`await`** keyword suspends the execution of the surrounding coroutine and passes control back to the event loop.
To clarify the last point a bit, when Python encounters an `await f()` expression in the scope of a `g()` coroutine, `await` tells the event loop: *suspend the execution of `g()` until the result of `f()` is returned. In the meantime, let something else run.*
In code, that last bullet point looks roughly like the following:
Python
```
```
Thereâs also a strict set of rules around when and how you can use `async` and `await`. These rules are helpful whether youâre still picking up the syntax or already have exposure to using `async` and `await`:
- Using the `async def` construct, you can define a coroutine function. It may use `await`, `return`, or `yield`, but all of these are optional:
- `await`, `return`, or both can be used in regular coroutine functions. To call a coroutine function, you must either `await` it to get its result or run it directly in an event loop.
- `yield` used in an `async def` function creates an asynchronous generator. To iterate over this generator, you can use an [`async for` loop or a comprehension](https://realpython.com/async-io-python/#async-iterators-loops-and-comprehensions).
- `async def` may not use `yield from`, which will raise a [`SyntaxError`](https://realpython.com/invalid-syntax-python/).
- Using `await` outside of an `async def` function also raises a `SyntaxError`. You can only use `await` in the body of coroutines.
Here are some terse examples that summarize these rules:
Python
```
```
Finally, when you use `await f()`, itâs required that `f()` be an object thatâs [**awaitable**](https://realpython.com/ref/glossary/awaitable/), which is either another coroutine or an object defining an `.__await__()` [special method](https://realpython.com/python-magic-methods/) that returns an iterator. For most purposes, you should only need to worry about coroutines.
Hereâs a more elaborate example of how async I/O cuts down on wait time. Suppose you have a coroutine function called `make_random()` that keeps producing random integers in the range \[0, 10\] and returns when one of them exceeds a threshold. In the following example, you run this function asynchronously three times. To differentiate each call, you use colors:
Python `rand.py`
```
```
The colorized output speaks louder than a thousand words. Hereâs how this script is carried out:
This program defines the `makerandom()` coroutine and runs it concurrently with three different inputs. Most programs will consist of small, modular coroutines and a wrapper function that serves to [chain](https://realpython.com/async-io-python/#coroutine-chaining) each smaller coroutine. In `main()`, you gather the three tasks. The three calls to `makerandom()` are your **pool of tasks**.
While the random number generation in this example is a CPU-bound task, its impact is negligible. The `asyncio.sleep()` simulates an I/O-bound task and makes the point that only I/O-bound or non-blocking tasks benefit from the async I/O model.
### The Async I/O Event Loop
In asynchronous programming, an event loop is like an [infinite loop](https://realpython.com/python-while-loop/#intentional-infinite-loops) that monitors coroutines, takes feedback on whatâs idle, and looks around for things that can be executed in the meantime. Itâs able to wake up an idle coroutine when whatever that coroutine is waiting for becomes available.
The recommended way to start an event loop in modern Python is to use [`asyncio.run()`](https://docs.python.org/3/library/asyncio-runner.html#asyncio.run). This function is responsible for getting the event loop, running tasks until they complete, and closing the loop. You canât call this function when another async event loop is running in the same code.
You can also get an instance of the running loop with the [`get_running_loop()`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop) function:
Python
```
loop = asyncio.get_running_loop()
```
If you need to interact with the event loop within a Python program, the above pattern is a good way to do it. The `loop` object supports introspection with `.is_running()` and `.is_closed()`. It can be useful when you want to [schedule a callback](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio-example-lowlevel-helloworld) by passing the loop as an argument, for example. Note that `get_running_loop()` raises a [`RuntimeError`](https://realpython.com/ref/builtin-exceptions/runtimeerror/) exception if thereâs no running event loop.
Whatâs more important is understanding what goes on beneath the surface of the event loop. Here are a few points worth stressing:
- Coroutines donât do much on their own until theyâre tied to the event loop.
- By default, an async event loop runs in a single thread and on a single CPU core. In most `asyncio` applications, there will be only one event loop, typically in the main thread. Running multiple event loops in different threads is technically possible, but not commonly needed or recommended.
- Event loops are pluggable. You can write your own implementation and have it run tasks just like the event loops provided in `asyncio`.
Regarding the first point, if you have a coroutine that awaits others, then calling it in isolation has little effect:
Python
```
```
In this example, calling `main()` directly returns a coroutine object that you canât use in isolation. You need to use `asyncio.run()` to schedule the `main()` coroutine for execution on the event loop:
Python
```
```
You typically wrap your `main()` coroutine in an `asyncio.run()` call. You can execute lower-level coroutines with `await`.
Finally, the fact that the event loop is *pluggable* means that you can use any working implementation of an event loop, and thatâs unrelated to your structure of coroutines. The `asyncio` package ships with two different [event loop implementations](https://docs.python.org/3/library/asyncio-eventloop.html#event-loop-implementations).
The default event loop implementation depends on your platform and Python version. For example, on Unix, the default is typically [`SelectorEventLoop`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.SelectorEventLoop), while Windows uses [`ProactorEventLoop`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.ProactorEventLoop) for better subprocess and I/O support.
Third-party event loops are also available. For example, the [uvloop](https://github.com/MagicStack/uvloop) package provides an alternative implementation that promises to be faster than the `asyncio` loops.
[Remove ads](https://realpython.com/account/join/)
### The `asyncio` REPL
Starting with [Python 3.8](https://realpython.com/python38-new-features/), the `asyncio` module includes a specialized interactive shell known as the [asyncio REPL](https://docs.python.org/3/library/asyncio.html#asyncio-cli). This environment allows you to use `await` directly at the top level, without wrapping your code in a call to `asyncio.run()`. This tool facilitates experimenting, debugging, and learning about `asyncio` in Python.
To start the [REPL](https://realpython.com/ref/glossary/repl/), you can run the following command:
Shell
```
```
Once you get the `>>>` prompt, you can start running asynchronous code there. Consider the example below, where you reuse the code from the previous section:
Python `Python 3.8+`
```
```
This example works the same as the one in the previous section. However, instead of running `main()` using `asyncio.run()`, you use `await` directly.
## Common Async I/O Programming Patterns
Async I/O has its own set of possible programming patterns that allow you to write better asynchronous code. In practice, you can *chain coroutines* or use a [queue](https://realpython.com/ref/glossary/queue/) of coroutines. Youâll learn how to use these two patterns in the following sections.
### Coroutine Chaining
A key feature of coroutines is that you can *chain* them together. Remember, a coroutine is awaitable, so another coroutine can await it using the `await` keyword. This makes it easier to break your program into smaller, manageable, and reusable coroutines.
The example below simulates a two-step process that fetches information about a user. The first step fetches the user information, and the second step fetches their published posts:
Python `chained.py`
```
```
In this example, you define two major coroutines: `fetch_user()` and `fetch_posts()`. Both simulate a network call with a random delay using `asyncio.sleep()`.
In the `fetch_user()` coroutine, you return a mock user [dictionary](https://realpython.com/python-dicts/). In `fetch_posts()`, you use that dictionary to return a list of mock posts attributed to the user at hand. Random delays simulate real-world asynchronous behavior like network latency.
The coroutine chaining happens in the `get_user_with_posts()`. This coroutine awaits `fetch_user()` and stores the result in the `user` [variable](https://realpython.com/python-variables/). Once the user information is available, itâs passed to `fetch_posts()` to retrieve the posts asynchronously.
In `main()`, you use `asyncio.gather()` to run the chained coroutines by executing `get_user_with_posts()` as many times as the number of user IDs you have.
Hereâs the result of executing the script:
Shell
```
```
If you sum up the time of all the operations, then this example would take around 7.6 seconds with a synchronous implementation. However, with the asynchronous implementation, it only takes 2.68 seconds.
The pattern, consisting of awaiting one coroutine and passing its result into the next, creates a **coroutine chain**, where each step depends on the previous one. This example mimics a common async workflow where you get one piece of information and use it to get related data.
[Remove ads](https://realpython.com/account/join/)
### Coroutine and Queue Integration
The `asyncio` package provides a few [queue-like classes](https://realpython.com/queue-in-python/#using-asynchronous-queues) that are designed to be similar to [classes](https://realpython.com/python-classes/) in the [`queue`](https://docs.python.org/3/library/queue.html#module-queue) module. In the examples so far, you havenât needed a queue structure. In `chained.py`, each task is performed by a coroutine, which you chain with others to pass data from one to the next.
An alternative approach is to use **producers** that add items to a [queue](https://realpython.com/ref/glossary/queue/). Each producer may add multiple items to the queue at staggered, random, and unannounced times. Then, a group of **consumers** pulls items from the queue as they show up, greedily and without waiting for any other signal.
In this design, thereâs no chaining between producers and consumers. Consumers donât know the number of producers, and vice versa.
It takes an individual producer or consumer a variable amount of time to add and remove items from the queue. The queue serves as a throughput that can communicate with the producers and consumers without them talking to each other directly.
A queue-based version of `chained.py` is shown below:
Python `queued.py`
```
```
In this example, the `producer()` function asynchronously fetches mock user data. Each fetched user dictionary is placed into an `asyncio.Queue` object, which shares the data with consumers. After producing all user objects, the producer inserts a [sentinel value](https://en.wikipedia.org/wiki/Sentinel_value)âalso known as a [poison pill](https://realpython.com/queue-in-python/#killing-a-worker-with-the-poison-pill) in this contextâfor each consumer to signal that no more data will be sent, allowing the consumers to shut down cleanly.
The `consumer()` function continuously reads from the queue. If it receives a user dictionary, it simulates fetching that userâs posts, waits a random delay, and prints the results. If it gets the sentinel value, then it breaks the loop and exits.
This decoupling allows multiple consumers to process users concurrently, even while the producer is still generating users, and the queue ensures safe and ordered communication between producers and consumers.
The queue is the communication point between the producers and consumers, enabling a scalable and responsive system.
Hereâs how the code works in practice:
Shell
```
```
Again, the code runs in only 2.68 seconds, which is more efficient than a synchronous solution. The result is pretty much the same as when you used chained coroutines in the previous section.
## Other Async I/O Features in Python
Pythonâs async I/O features extend beyond the `async def` and `await` constructs. They include other advanced tools that make asynchronous programming more expressive and consistent with regular Python constructs.
In the following sections, youâll explore powerful async features, including async loops and comprehensions, the `async with` statement, and exception groups. These features will help you write cleaner, more readable asynchronous code.
### Async Iterators, Loops, and Comprehensions
Apart from using `async` and `await` to create coroutines, Python also provides the `async for` construct to iterate over an [**asynchronous iterator**](https://realpython.com/ref/glossary/asynchronous-iterator/). An asynchronous iterator allows you to iterate over asynchronously generated data. While the loop runs, it gives control back to the event loop so that other async tasks can run.
**Note:** To learn more about async iterators, check out the [Asynchronous Iterators and Iterables in Python](https://realpython.com/python-async-iterators/) tutorial.
A natural extension of this concept is an [**asynchronous generator**](https://realpython.com/ref/glossary/asynchronous-generator/). Hereâs an example that generates powers of two and uses them in a loop and comprehension:
Python
```
```
Thereâs a crucial distinction between synchronous and asynchronous generators, loops, and comprehensions. Their asynchronous counterparts donât inherently make iteration concurrent. Instead, they allow the event loop to run other tasks between iterations when you explicitly yield control by using `await`. The iteration itself is still sequential unless you introduce concurrency by using `asyncio.gather()`.
Using `async for` and `async with` is only required when working with asynchronous iterators or context managers, where a regular `for` or `with` would raise errors.
[Remove ads](https://realpython.com/account/join/)
### Async `with` Statements
The [`with` statement](https://realpython.com/python-with-statement/) also has an [asynchronous](https://realpython.com/ref/glossary/asynchronous-programming/) version, `async with`. This construct is quite common in async code, as many [I/O-bound tasks](https://realpython.com/ref/glossary/io-bound-task/) involve setup and teardown phases.
For example, say you need to write a coroutine to check whether some websites are online. To do that, you can use [`aiohttp`](https://docs.aiohttp.org/en/stable/index.html), which is a third-party library that you need to install by running `python -m pip install aiohttp` on your command line.
Hereâs a quick example that implements the required functionality:
Python
```
```
In this example, you use `aiohttp` and `asyncio` to perform concurrent [HTTP GET](https://realpython.com/api-integration-in-python/#get) requests to a list of websites. The `check()` coroutine fetches and prints the websiteâs status. The `async with` statement ensures that both `ClientSession` and the individual HTTP response are properly and asynchronously managed by opening and closing them without blocking the event loop.
In this example, using `async with` guarantees that the underlying network resources, including connections and sockets, are correctly released, even if an error occurs.
Finally, `main()` runs the `check()` coroutines concurrently, allowing you to fetch the URLs in parallel without waiting for one to finish before starting the next.
### Other `asyncio` Tools
In addition to `asyncio.run()`, youâve used a few other package-level functions, such as `asyncio.gather()` and `asyncio.get_event_loop()`. You can use [`asyncio.create_task()`](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) to schedule the execution of a coroutine object, followed by the usual call to the `asyncio.run()` function:
Python
```
```
This pattern includes a subtle detail you need to be aware of: if you create tasks with `create_task()` but donât await them or wrap them in `gather()`, and your `main()` coroutine finishes, then those manually created tasks will be canceled when the event loop ends. You must await all tasks you want to complete.
The `create_task()` function wraps an awaitable object into a higher-level [`Task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task) object thatâs scheduled to run concurrently on the event loop in the background. In contrast, awaiting a coroutine runs it immediately, pausing the execution of the caller until the awaited coroutine finishes.
The `gather()` function is meant to neatly put a collection of coroutines into a single **future object**. This object represents a placeholder for a result thatâs initially unknown but will be available at some point, typically as the result of asynchronous computations.
If you await `gather()` and specify multiple tasks or coroutines, then the loop will wait for all the tasks to complete. The result of `gather()` will be a list of the results across the inputs:
Python
```
```
You probably noticed that `gather()` waits for the entire result of the whole set of coroutines that you pass it. The order of results from `gather()` is deterministic and corresponds to the order of awaitables originally passed to it.
Alternatively, you can loop over `asyncio.as_completed()` to get tasks as they complete. The function returns a synchronous iterator that yields tasks as they finish. Below, the result of `coro([3, 2, 1])` will be available before `coro([10, 5, 2])` is complete, which wasnât the case with the `gather()` function:
Python
```
```
In this example, the `main()` function uses `asyncio.as_completed()`, which yields tasks in the order they complete, not in the order they were started. As the program loops through the tasks, it awaits them, allowing the results to be available immediately upon completion.
As a result, the faster task (`task1`) finishes first and its result is printed earlier, while the longer task (`task2`) completes and prints afterward. The `as_completed()` function is useful when you need to handle tasks dynamically as they finish, which improves responsiveness in concurrent workflows.
[Remove ads](https://realpython.com/account/join/)
### Async Exception Handling
Starting with [Python 3.11](https://realpython.com/python311-new-features/), you can use the [`ExceptionGroup`](https://realpython.com/python311-exception-groups/) class to handle multiple unrelated exceptions that may occur concurrently. This is especially useful when running multiple coroutines that can raise different exceptions. Additionally, the new `except*` syntax helps you gracefully deal with several errors at once.
Hereâs a quick demo of how to use this class in asynchronous code:
Python `Python 3.11+`
```
```
In this example, you have three coroutines that raise three different types of [exceptions](https://realpython.com/python-built-in-exceptions/). In the `main()` function, you call `gather()` with the coroutines as arguments. You also set the `return_exceptions` argument to `True` so that you can grab the exceptions if they occur.
Next, you use a list comprehension to store the exceptions in a new list. If the list contains at least one exception, then you create an `ExceptionGroup` for them.
To handle this exception group, you can use the following code:
Python `Python 3.11+`
```
```
In this code, you wrap the call to `asyncio.run()` in a [`try`](https://realpython.com/ref/keywords/try/) block. Then, you use the `except*` syntax to catch the expected exception separately. In each case, you print an error message to the screen.
## Async I/O in Context
Now that youâve seen a healthy dose of asynchronous code, take a moment to step back and consider when async I/O is the ideal choiceâand how to evaluate whether itâs the right fit or if another concurrency model might be better.
### When to Use Async I/O
Using `async def` for functions that perform blocking operationsâsuch as standard file I/O or synchronous network requestsâwill block the entire event loop, negate the benefits of async I/O, and potentially reduce your programâs efficiency. Only use `async def` functions for [non-blocking operations](https://realpython.com/ref/glossary/non-blocking-operation/).
The battle between async I/O and multiprocessing isnât a real battle. You can use both models [in concert](https://youtu.be/0kXaLh8Fz3k?t=10m30s) if you want. In practice, multiprocessing should be the right choice if you have multiple CPU-bound tasks.
The contest between async I/O and threading is more direct. Threading isnât simple, and even in cases where threading seems easy to implement, it can still lead to hard-to-trace bugs due to [race conditions](https://realpython.com/python-thread-lock/#race-conditions) and memory usage, among other things.
Threading also tends to scale less elegantly than async I/O because threads are a system resource with a finite availability. Creating thousands of threads will fail on many machines or can slow down your code. In contrast, creating thousands of async I/O tasks is completely feasible.
Async I/O shines when you have multiple I/O-bound tasks that would otherwise be dominated by blocking wait time, such as:
- **Network I/O**, whether your program is acting as the server or the client
- **Serverless designs**, such as a peer-to-peer, multi-user network like a group chat
- **Read/write operations** where you want to mimic a [fire-and-forget](https://en.wikipedia.org/wiki/Fire-and-forget) style approach without worrying about holding a lock on the resource
The biggest reason not to use async I/O is that `await` only supports a specific set of objects that define a particular set of methods. For example, if you want to do async read operations on a certain [database management system (DBMS)](https://en.wikipedia.org/wiki/Database#Database_management_system), then youâll need to find a Python wrapper for that DBMS that supports the `async` and `await` syntax.
### Libraries Supporting Async I/O
Youâll find several high-quality third-party libraries and frameworks that support or are built on top of `asyncio` in Python, including tools for web servers, databases, networking, testing, and more. Here are some of the most notable:
- **Web frameworks:**
- [FastAPI](https://fastapi.tiangolo.com/): Modern async web framework for building [web APIs](https://realpython.com/python-api/).
- [Starlette](https://www.starlette.io/): Lightweight [asynchronous server gateway interface (ASGI)](https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface) framework for building high-performance async web apps.
- [Sanic](https://sanic.dev/): Async web framework built for speed using `asyncio`.
- [Quart](https://github.com/pallets/quart): Async web microframework with the same API as [Flask](https://realpython.com/flask-project/).
- [Tornado](https://github.com/tornadoweb/tornado): Performant web framework and asynchronous networking library.
- **ASGI servers:**
- [uvicorn](https://www.uvicorn.org/): Fast ASGI web server.
- [Hypercorn](https://pypi.org/project/Hypercorn/): ASGI server supporting several protocols and configuration options.
- **Networking tools:**
- [aiohttp](https://docs.aiohttp.org/): HTTP client and server implementation using `asyncio`.
- [HTTPX](https://www.python-httpx.org/): Fully featured async and sync HTTP client.
- [websockets](https://websockets.readthedocs.io/): Library for building WebSocket servers and clients with `asyncio`.
- [aiosmtplib](https://aiosmtplib.readthedocs.io/): Async SMTP client for [sending emails](https://realpython.com/python-send-email/).
- **Database tools:**
- [Databases](https://www.encode.io/databases/): Async database access layer compatible with [SQLAlchemy](https://realpython.com/python-sqlite-sqlalchemy/) core.
- [Tortoise ORM](https://tortoise.github.io/): Lightweight async object-relational mapper (ORM).
- [Gino](https://python-gino.org/): Async ORM built on SQLAlchemy core for [PostgreSQL](https://realpython.com/python-sql-libraries/#postgresql).
- [Motor](https://motor.readthedocs.io/): Async [MongoDB](https://realpython.com/introduction-to-mongodb-and-python/) driver built on `asyncio`.
- **Utility libraries:**
- [aiofiles](https://github.com/Tinche/aiofiles): Wraps Pythonâs file API for use with `async` and `await`.
- [aiocache](https://github.com/aio-libs/aiocache): Async caching library supporting [Redis](https://realpython.com/python-redis/) and Memcached.
- [APScheduler](https://github.com/agronholm/apscheduler): A task scheduler with support for async jobs.
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/): Adds support for testing async functions using [pytest](https://realpython.com/pytest-python-testing/).
These libraries and frameworks help you write performant async Python applications. Whether youâre building a web server, fetching data over the network, or accessing a database, `asyncio` tools like these give you the power to handle many tasks concurrently with minimal overhead.
[Remove ads](https://realpython.com/account/join/)
## Conclusion
Youâve gained a solid understanding of Pythonâs `asyncio` library and the `async` and `await` syntax, learning how asynchronous programming enables efficient management of multiple I/O-bound tasks within a single thread.
Along the way, you explored the differences between concurrency, parallelism, threading, multiprocessing, and asynchronous I/O. You also worked through practical examples using coroutines, event loops, chaining, and queue-based concurrency. On top of that, you learned about advanced `asyncio` features, including async context managers, async iterators, comprehensions, and how to leverage third-party async libraries.
Mastering `asyncio` is essential when building scalable network servers, web APIs, or applications that perform many simultaneous I/O-bound operations.
**In this tutorial, youâve learned how to:**
- **Distinguish** between concurrency models and identify when to use **`asyncio`** for I/O-bound tasks
- **Write**, **run**, and **chain coroutines** using `async def` and `await`
- **Manage the event loop** and schedule multiple tasks with `asyncio.run()`, `gather()`, and `create_task()`
- Implement async patterns like **coroutine chaining** and **async queues** for producerâconsumer workflows
- **Use advanced async features** such as `async for` and `async with`, and integrate with **third-party async libraries**
With these skills, youâre ready to build high-performance, modern Python applications that can handle many operations asynchronously.
**Get Your Code:** [Click here to download the free sample code](https://realpython.com/bonus/async-io-python-code/) that youâll use to learn about async I/O in Python.
## Frequently Asked Questions
Now that you have some experience with `asyncio` in Python, you can use the questions and answers below to check your understanding and recap what youâve learned.
These FAQs are related to the most important concepts youâve covered in this tutorial. Click the *Show/Hide* toggle beside each question to reveal the answer.
**What is `asyncio` in Python and why should you use it?**Show/Hide
You use `asyncio` to write concurrent code with the `async` and `await` keywords, allowing you to efficiently manage multiple I/O-bound tasks in a single thread without blocking your program.
**Is `asyncio` better than multithreading for I/O-bound tasks?**Show/Hide
You typically get better performance from `asyncio` for I/O-bound work because it avoids the overhead and complexity of threads. This allows thousands of tasks to run concurrently without the limitations of Pythonâs GIL.
**When should you use `asyncio` in your Python programs?**Show/Hide
Use `asyncio` when your program spends a significant amount of time waiting on I/O-bound operationsâsuch as network requests or file accessâand you want to run many of these tasks concurrently and efficiently.
**How do you define and run a coroutine with `asyncio`?**Show/Hide
You define a coroutine using the `async def` syntax. To run it, either pass it to `asyncio.run()` or schedule it as a task with `asyncio.create_task()`.
**What role does the event loop play in `asyncio`?**Show/Hide
You rely on the event loop to manage the scheduling and execution of your coroutines, giving each one a chance to run whenever it awaits or completes an I/O-bound operation.
***Take the Quiz:*** Test your knowledge with our interactive âPython's asyncio: A Hands-On Walkthroughâ quiz. Youâll receive a score upon completion to help you track your learning progress:
***
[](https://realpython.com/quizzes/async-io-python/)
**Interactive Quiz**
[Python's asyncio: A Hands-On Walkthrough](https://realpython.com/quizzes/async-io-python/)
Test your knowledge of \`asyncio\` concurrency with this quiz that covers coroutines, event loops, and efficient I/O-bound task management.
Mark as Completed
Share
Recommended Course
[Hands-On Python 3 Concurrency With the asyncio Module](https://realpython.com/courses/python-3-concurrency-asyncio-module/) (1h 48m)
đ Python Tricks đ
Get a short & sweet **Python Trick** delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

About **Leodanis Pozo Ramos**
[ ](https://realpython.com/team/lpozoramos/)
Leodanis is a self-taught Python developer, educator, and technical writer with over 10 years of experience.
[» More about Leodanis](https://realpython.com/team/lpozoramos/)
***
*Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:*
[](https://realpython.com/team/asantos/)
[Aldren](https://realpython.com/team/asantos/)
[](https://realpython.com/team/bsolomon/)
[Brad](https://realpython.com/team/bsolomon/)
[](https://realpython.com/team/bweleschuk/)
[Brenda](https://realpython.com/team/bweleschuk/)
[](https://realpython.com/team/bzaczynski/)
[Bartosz](https://realpython.com/team/bzaczynski/)
[](https://realpython.com/team/damos/)
[David](https://realpython.com/team/damos/)
[](https://realpython.com/team/jjablonski/)
[Joanna](https://realpython.com/team/jjablonski/)
Master Real-World Python Skills With Unlimited Access to Real Python

**Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:**
[Level Up Your Python Skills »](https://realpython.com/account/join/?utm_source=rp_article_footer&utm_content=async-io-python)
Master Real-World Python Skills
With Unlimited Access to Real Python

**Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:**
[Level Up Your Python Skills »](https://realpython.com/account/join/?utm_source=rp_article_footer&utm_content=async-io-python)
What Do You Think?
**Rate this article:**
[LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Frealpython.com%2Fasync-io-python%2F)
[Twitter](https://twitter.com/intent/tweet/?text=Interesting%20Python%20article%20on%20%40realpython%3A%20Python%27s%20asyncio%3A%20A%20Hands-On%20Walkthrough&url=https%3A%2F%2Frealpython.com%2Fasync-io-python%2F)
[Bluesky](https://bsky.app/intent/compose?text=Interesting%20Python%20article%20on%20%40realpython.com%3A%20Python%27s%20asyncio%3A%20A%20Hands-On%20Walkthrough%20https%3A%2F%2Frealpython.com%2Fasync-io-python%2F)
[Facebook](https://facebook.com/sharer/sharer.php?u=https%3A%2F%2Frealpython.com%2Fasync-io-python%2F)
[Email](mailto:?subject=Python%20article%20for%20you&body=Python%27s%20asyncio%3A%20A%20Hands-On%20Walkthrough%20on%20Real%20Python%0A%0Ahttps%3A%2F%2Frealpython.com%2Fasync-io-python%2F%0A)
Whatâs your \#1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.
**Commenting Tips:** The most useful comments are those written with the goal of learning from or helping out other students. [Get tips for asking good questions](https://realpython.com/python-beginner-tips/#tip-9-ask-good-questions) and [get answers to common questions in our support portal](https://support.realpython.com/).
***
Looking for a real-time conversation? Visit the [Real Python Community Chat](https://realpython.com/community/) or join the next [âOffice Hoursâ Live Q\&A Session](https://realpython.com/office-hours/). Happy Pythoning\!
Keep Learning
Related Topics: [advanced](https://realpython.com/tutorials/advanced/) [python](https://realpython.com/tutorials/python/)
Related Learning Paths:
- [Concurrency and Async Programming](https://realpython.com/learning-paths/python-concurrency-parallel-programming/?utm_source=realpython&utm_medium=web&utm_campaign=related-learning-path&utm_content=async-io-python)
Related Courses:
- [Hands-On Python 3 Concurrency With the asyncio Module](https://realpython.com/courses/python-3-concurrency-asyncio-module/?utm_source=realpython&utm_medium=web&utm_campaign=related-course&utm_content=async-io-python)
Related Tutorials:
- [Speed Up Your Python Program With Concurrency](https://realpython.com/python-concurrency/?utm_source=realpython&utm_medium=web&utm_campaign=related-post&utm_content=async-io-python)
- [Primer on Python Decorators](https://realpython.com/primer-on-python-decorators/?utm_source=realpython&utm_medium=web&utm_campaign=related-post&utm_content=async-io-python)
- [An Intro to Threading in Python](https://realpython.com/intro-to-python-threading/?utm_source=realpython&utm_medium=web&utm_campaign=related-post&utm_content=async-io-python)
- [What Is the Python Global Interpreter Lock (GIL)?](https://realpython.com/python-gil/?utm_source=realpython&utm_medium=web&utm_campaign=related-post&utm_content=async-io-python)
- [Asynchronous Iterators and Iterables in Python](https://realpython.com/python-async-iterators/?utm_source=realpython&utm_medium=web&utm_campaign=related-post&utm_content=async-io-python)
## Keep reading Real Python by creating a free account or signing in:
[](https://realpython.com/account/signup/?intent=continue_reading&utm_source=rp&utm_medium=web&utm_campaign=rwn&utm_content=v1&next=%2Fasync-io-python%2F)
[Continue »](https://realpython.com/account/signup/?intent=continue_reading&utm_source=rp&utm_medium=web&utm_campaign=rwn&utm_content=v1&next=%2Fasync-io-python%2F)
Already have an account? [Sign-In](https://realpython.com/account/login/?next=/async-io-python/)
Almost there! Complete this form and click the button below to gain instant access:
Ă

Async I/O in Python: A Complete Walkthrough (Sample Code)
##### Learn Python
- [Start Here](https://realpython.com/start-here/)
- [Learning Resources](https://realpython.com/search)
- [Code Mentor](https://realpython.com/mentor/)
- [Python Reference](https://realpython.com/ref/)
- [Python Cheat Sheet](https://realpython.com/cheatsheets/python/)
- [Support Center](https://support.realpython.com/)
##### Courses & Paths
- [Learning Paths](https://realpython.com/learning-paths/)
- [Quizzes & Exercises](https://realpython.com/quizzes/)
- [Browse Topics](https://realpython.com/tutorials/all/)
- [Live Courses](https://realpython.com/live/)
- [Books](https://realpython.com/books/)
##### Community
- [Podcast](https://realpython.com/podcasts/rpp/)
- [Newsletter](https://realpython.com/newsletter/)
- [Community Chat](https://realpython.com/community/)
- [Office Hours](https://realpython.com/office-hours/)
- [Learner Stories](https://realpython.com/learner-stories/)
##### Membership
- [Plans & Pricing](https://realpython.com/account/join/)
- [Team Plans](https://realpython.com/account/join-team/)
- [For Business](https://realpython.com/account/join-team/inquiry/)
- [For Schools](https://realpython.com/account/join-team/education-inquiry/)
- [Reviews](https://realpython.com/learner-stories/)
##### Company
- [About Us](https://realpython.com/about/)
- [Team](https://realpython.com/team/)
- [Mission & Values](https://realpython.com/mission/)
- [Editorial Guidelines](https://realpython.com/editorial-guidelines/)
- [Sponsorships](https://realpython.com/sponsorships/)
- [Careers](https://realpython.workable.com/)
- [Press Kit](https://realpython.com/media-kit/)
- [Merch](https://realpython.com/merch)
[Privacy Policy](https://realpython.com/privacy-policy/) â
[Terms of Use](https://realpython.com/terms/) â
[Security](https://realpython.com/security/) â
[Contact](https://realpython.com/contact/)
Happy Pythoning\!
© 2012â2026 DevCademy Media Inc. DBA Real Python. All rights reserved.
REALPYTHONâą is a trademark of DevCademy Media Inc.
[](https://realpython.com/)

You've blocked notifications |
| Readable Markdown | by [Leodanis Pozo Ramos](https://realpython.com/async-io-python/#author) Publication date Jul 30, 2025 Reading time estimate 38m [advanced](https://realpython.com/tutorials/advanced/) [python](https://realpython.com/tutorials/python/)
Pythonâs `asyncio` library enables you to write concurrent code using the `async` and `await` keywords. The core building blocks of async I/O in Python are awaitable objectsâmost often coroutinesâthat an event loop schedules and executes asynchronously. This programming model lets you efficiently manage multiple I/O-bound tasks within a single thread of execution.
In this tutorial, youâll learn how Python `asyncio` works, how to define and run coroutines, and when to use asynchronous programming for better performance in applications that perform I/O-bound tasks.
**By the end of this tutorial, youâll understand that:**
- Pythonâs **`asyncio`** provides a framework for writing single-threaded **concurrent code** using **coroutines**, **event loops**, and **non-blocking I/O operations**.
- For I/O-bound tasks, async I/O **can often outperform multithreading**âespecially when managing a large number of concurrent tasksâbecause it avoids the overhead of thread management.
- You should use `asyncio` when your application spends significant time waiting on **I/O operations**, such as network requests or file access, and you want to **run many of these tasks concurrently** without creating extra threads or processes.
Through hands-on examples, youâll gain the practical skills to write efficient Python code using `asyncio` that scales gracefully with increasing I/O demands.
***Take the Quiz:*** Test your knowledge with our interactive âPython's asyncio: A Hands-On Walkthroughâ quiz. Youâll receive a score upon completion to help you track your learning progress:
***
[](https://realpython.com/quizzes/async-io-python/)
## A First Look at Async I/O
Before exploring `asyncio`, itâs worth taking a moment to compare async I/O with other concurrency models to see how it fits into Pythonâs broader, sometimes dizzying, landscape. Here are some essential concepts to start with:
- **Parallelism** consists of executing multiple operations at the same time.
- **Multiprocessing** is a means of achieving parallelism that entails spreading tasks over a computerâs central processing unit (CPU) cores. Multiprocessing is well-suited for CPU-bound tasks, such as tightly bound [`for` loops](https://realpython.com/python-for-loop/) and mathematical computations.
- **Concurrency** is a slightly broader term than parallelism, suggesting that multiple tasks have the ability to run in an overlapping manner. Concurrency doesnât necessarily imply parallelism.
- **Threading** is a concurrent execution model in which multiple threads take turns executing tasks. A single process can contain multiple threads. Pythonâs relationship with threading is complicated due to the [global interpreter lock (GIL)](https://realpython.com/python-gil/), but thatâs beyond the scope of this tutorial.
Threading is good for [**I/O-bound tasks**](https://realpython.com/ref/glossary/io-bound-task/). An I/O-bound job is dominated by a lot of waiting on [**input/output (I/O)**](https://realpython.com/ref/glossary/input-output/) to complete, while a [CPU-bound task](https://realpython.com/ref/glossary/cpu-bound-task/) is characterized by the computerâs cores continually working hard from start to finish.
The Python [standard library](https://realpython.com/ref/glossary/standard-library/) has offered longstanding [support for these models](https://docs.python.org/3/library/concurrency.html) through its `multiprocessing`, `concurrent.futures`, and `threading` packages.
Now itâs time to add a new member to the mix. In recent years, a separate model has been more comprehensively built into [CPython](https://realpython.com/cpython-source-code-guide/): **asynchronous I/O**, commonly called **async I/O**. This model is enabled through the standard libraryâs [**`asyncio`**](https://realpython.com/ref/stdlib/asyncio/) package and the [`async`](https://realpython.com/python-keywords/#the-async-keyword) and [`await`](https://realpython.com/python-keywords/#the-await-keyword) keywords.
The `asyncio` package is billed by the Python documentation as a [library to write concurrent code](https://docs.python.org/3/library/asyncio.html). However, async I/O isnât threading or multiprocessing. Itâs not built on top of either of these.
Async I/O is a single-threaded, single-process technique that uses [cooperative multitasking](https://en.wikipedia.org/wiki/Cooperative_multitasking). Async I/O gives a feeling of concurrency despite using a single thread in a single process. [Coroutines](https://realpython.com/ref/glossary/coroutine/)âor **coro** for shortâare a central feature of async I/O and can be scheduled concurrently, but theyâre not inherently concurrent.
To reiterate, async I/O is a model of concurrent programming, but itâs not parallelism. Itâs more closely aligned with threading than with multiprocessing, but itâs different from both and is a standalone member of the concurrency ecosystem.
That leaves one more term. What does it mean for something to be **asynchronous**? This isnât a rigorous definition, but for the purposes of this tutorial, you can think of two key properties:
1. **Asynchronous routines** can *pause* their execution while waiting for a result and allow other routines to run in the meantime.
2. **Asynchronous code** facilitates the concurrent execution of tasks by coordinating asynchronous routines.
Hereâs a diagram that puts it all together. The white terms represent concepts, and the green terms represent the ways theyâre implemented:
[](https://files.realpython.com/media/Screen_Shot_2018-10-17_at_3.18.44_PM.c02792872031.jpg)
Diagram Comparing Concurrency and Parallelism in Python (Threading, Async I/O, Multiprocessing)
For a thorough exploration of threading versus multiprocessing versus async I/O, pause here and check out the [Speed Up Your Python Program With Concurrency](https://realpython.com/python-concurrency/) tutorial. For now, youâll focus on async I/O.
### Async I/O Explained
Async I/O may seem counterintuitive and paradoxical at first. How does something that facilitates concurrent code use a single thread in a single CPU core? Miguel Grinbergâs [PyCon](https://realpython.com/pycon-guide/) talk explains everything quite beautifully:
> Chess master Judit PolgĂĄr hosts a chess exhibition in which she plays multiple amateur players. She has two ways of conducting the exhibition: *synchronously* and *asynchronously*.
>
> Assumptions:
>
> - 24 opponents
> - Judit makes each chess move in 5 seconds
> - Opponents each take 55 seconds to make a move
> - Games average 30 pair-moves (60 moves total)
>
> **Synchronous version**: Judit plays one game at a time, never two at the same time, until the game is complete. Each game takes *(55 + 5) \* 30 == 1800* seconds, or 30 minutes. The entire exhibition takes *24 \* 30 == 720* minutes, or **12 hours**.
>
> **Asynchronous version**: Judit moves from table to table, making one move at each table. She leaves the table and lets the opponent make their next move during the wait time. One move on all 24 games takes Judit *24 \* 5 == 120* seconds, or 2 minutes. The entire exhibition is now cut down to *120 \* 30 == 3600* seconds, or just **1 hour**. ([Source](https://youtu.be/iG6fr81xHKA?t=4m29s))
Thereâs only one Judit PolgĂĄr, who makes only one move at a time. Playing asynchronously cuts the exhibition time down from 12 hours to 1 hour. Async I/O applies this principle to programming. In async I/O, a programâs event loopâmore on that laterâruns multiple tasks, allowing each to take turns running at the optimal time.
Async I/O takes long-running [functions](https://realpython.com/defining-your-own-python-function/)âlike a complete chess game in the example aboveâthat would block a programâs execution (Judit PolgĂĄrâs time). It manages them in a way so other functions can run during that downtime. In the chess example, Judit PolgĂĄr plays with another participant while the previous ones make their moves.
### Async I/O Isnât Simple
Building durable multithreaded code can be challenging and prone to errors. Async I/O avoids some of the potential speed bumps you might encounter with a multithreaded design. However, thatâs not to say that [asynchronous programming](https://realpython.com/ref/glossary/asynchronous-programming/) is a simple task in Python.
Be aware that async programming can get tricky when you venture a bit below the surface level. Pythonâs async model is built around concepts such as callbacks, coroutines, events, transports, protocols, and [futures](https://docs.python.org/3/library/asyncio-future.html#asyncio.Future)âeven just the terminology can be intimidating.
That said, the ecosystem around async programming in Python has improved significantly. The `asyncio` package has matured and now provides a stable [API](https://realpython.com/ref/glossary/api/). Additionally, its documentation has received a considerable overhaul, and some high-quality resources on the subject have also emerged.
## Async I/O in Python With `asyncio`
Now that you have some background on async I/O as a concurrency model, itâs time to explore Pythonâs implementation. Pythonâs `asyncio` package and its two related keywords, [`async`](https://realpython.com/python-keywords/#the-async-keyword) and [`await`](https://realpython.com/python-keywords/#the-await-keyword), serve different purposes but come together to help you declare, build, execute, and manage asynchronous code.
### Coroutines and Coroutine Functions
At the heart of async I/O is the concept of a [**coroutine**](https://realpython.com/ref/glossary/coroutine/), which is an object that can suspend its execution and resume it later. In the meantime, it can pass the control to an event loop, which can execute another coroutine. Coroutine objects result from calling a [**coroutine function**](https://realpython.com/ref/glossary/coroutine-function/), also known as an **asynchronous function**. You define one with the `async def` construct.
Before writing your first piece of asynchronous code, consider the following example that runs synchronously:
The `count()` function [prints](https://realpython.com/python-print/) `One` and waits for a second, then prints `Two` and waits for another second. The loop in the [`main()`](https://realpython.com/python-main-function/) function executes `count()` three times. Below, in the [`if __name__ == "__main__"`](https://realpython.com/if-name-main-python/) condition, you take a snapshot of the current time at the beginning of the execution, call `main()`, compute the total time, and display it on the screen.
When you [run this script](https://realpython.com/run-python-scripts/), youâll get the following output:
The script prints `One` and `Two` alternatively, taking a second between each printing operation. In total, it takes a bit more than six seconds to run.
If you update this script to use Pythonâs async I/O model, then it would look something like the following:
Now, you use the `async` keyword to turn `count()` into a coroutine function that prints `One`, waits for one second, then prints `Two`, and waits another second. You use the `await` keyword to *await* the execution of `asyncio.sleep()`. This gives the control back to the programâs event loop, saying: *I will sleep for one second. Go ahead and run something else in the meantime.*
The `main()` function is another coroutine function that uses [`asyncio.gather()`](https://realpython.com/async-io-python/#other-asyncio-tools) to run three instances of `count()` concurrently. You use the `asyncio.run()` function to launch the [event loop](https://realpython.com/async-io-python/#the-async-io-event-loop) and execute `main()`.
Compare the performance of this version to that of the synchronous version:
Thanks to the async I/O approach, the total execution time is just over two seconds instead of six, demonstrating the efficiency of `asyncio` for I/O-bound tasks.
While using `time.sleep()` and `asyncio.sleep()` may seem banal, they serve as stand-ins for time-intensive processes that involve wait time. A call to `time.sleep()` can represent a time-consuming blocking function call, while `asyncio.sleep()` is used to stand in for a [non-blocking call](https://realpython.com/ref/glossary/non-blocking-operation/) that also takes some time to complete.
As youâll see in the next section, the benefit of awaiting something, including `asyncio.sleep()`, is that the surrounding function can temporarily cede control to another function thatâs more readily able to do something immediately. In contrast, `time.sleep()` or any other blocking call is incompatible with asynchronous Python code because it stops everything in its tracks for the duration of the sleep time.
### The `async` and `await` Keywords
At this point, a more formal definition of `async`, `await`, and the coroutine functions they help you create is in order:
- The **`async def`** syntax construct introduces either a **coroutine function** or an [**asynchronous generator**](https://realpython.com/ref/glossary/asynchronous-generator/).
- The **`async with`** and **`async for`** syntax constructs introduce asynchronous **`with` statements** and **`for` loops**, respectively.
- The **`await`** keyword suspends the execution of the surrounding coroutine and passes control back to the event loop.
To clarify the last point a bit, when Python encounters an `await f()` expression in the scope of a `g()` coroutine, `await` tells the event loop: *suspend the execution of `g()` until the result of `f()` is returned. In the meantime, let something else run.*
In code, that last bullet point looks roughly like the following:
Thereâs also a strict set of rules around when and how you can use `async` and `await`. These rules are helpful whether youâre still picking up the syntax or already have exposure to using `async` and `await`:
- Using the `async def` construct, you can define a coroutine function. It may use `await`, `return`, or `yield`, but all of these are optional:
- `await`, `return`, or both can be used in regular coroutine functions. To call a coroutine function, you must either `await` it to get its result or run it directly in an event loop.
- `yield` used in an `async def` function creates an asynchronous generator. To iterate over this generator, you can use an [`async for` loop or a comprehension](https://realpython.com/async-io-python/#async-iterators-loops-and-comprehensions).
- `async def` may not use `yield from`, which will raise a [`SyntaxError`](https://realpython.com/invalid-syntax-python/).
- Using `await` outside of an `async def` function also raises a `SyntaxError`. You can only use `await` in the body of coroutines.
Here are some terse examples that summarize these rules:
Finally, when you use `await f()`, itâs required that `f()` be an object thatâs [**awaitable**](https://realpython.com/ref/glossary/awaitable/), which is either another coroutine or an object defining an `.__await__()` [special method](https://realpython.com/python-magic-methods/) that returns an iterator. For most purposes, you should only need to worry about coroutines.
Hereâs a more elaborate example of how async I/O cuts down on wait time. Suppose you have a coroutine function called `make_random()` that keeps producing random integers in the range \[0, 10\] and returns when one of them exceeds a threshold. In the following example, you run this function asynchronously three times. To differentiate each call, you use colors:
The colorized output speaks louder than a thousand words. Hereâs how this script is carried out:
This program defines the `makerandom()` coroutine and runs it concurrently with three different inputs. Most programs will consist of small, modular coroutines and a wrapper function that serves to [chain](https://realpython.com/async-io-python/#coroutine-chaining) each smaller coroutine. In `main()`, you gather the three tasks. The three calls to `makerandom()` are your **pool of tasks**.
While the random number generation in this example is a CPU-bound task, its impact is negligible. The `asyncio.sleep()` simulates an I/O-bound task and makes the point that only I/O-bound or non-blocking tasks benefit from the async I/O model.
### The Async I/O Event Loop
In asynchronous programming, an event loop is like an [infinite loop](https://realpython.com/python-while-loop/#intentional-infinite-loops) that monitors coroutines, takes feedback on whatâs idle, and looks around for things that can be executed in the meantime. Itâs able to wake up an idle coroutine when whatever that coroutine is waiting for becomes available.
The recommended way to start an event loop in modern Python is to use [`asyncio.run()`](https://docs.python.org/3/library/asyncio-runner.html#asyncio.run). This function is responsible for getting the event loop, running tasks until they complete, and closing the loop. You canât call this function when another async event loop is running in the same code.
You can also get an instance of the running loop with the [`get_running_loop()`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop) function:
If you need to interact with the event loop within a Python program, the above pattern is a good way to do it. The `loop` object supports introspection with `.is_running()` and `.is_closed()`. It can be useful when you want to [schedule a callback](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio-example-lowlevel-helloworld) by passing the loop as an argument, for example. Note that `get_running_loop()` raises a [`RuntimeError`](https://realpython.com/ref/builtin-exceptions/runtimeerror/) exception if thereâs no running event loop.
Whatâs more important is understanding what goes on beneath the surface of the event loop. Here are a few points worth stressing:
- Coroutines donât do much on their own until theyâre tied to the event loop.
- By default, an async event loop runs in a single thread and on a single CPU core. In most `asyncio` applications, there will be only one event loop, typically in the main thread. Running multiple event loops in different threads is technically possible, but not commonly needed or recommended.
- Event loops are pluggable. You can write your own implementation and have it run tasks just like the event loops provided in `asyncio`.
Regarding the first point, if you have a coroutine that awaits others, then calling it in isolation has little effect:
In this example, calling `main()` directly returns a coroutine object that you canât use in isolation. You need to use `asyncio.run()` to schedule the `main()` coroutine for execution on the event loop:
You typically wrap your `main()` coroutine in an `asyncio.run()` call. You can execute lower-level coroutines with `await`.
Finally, the fact that the event loop is *pluggable* means that you can use any working implementation of an event loop, and thatâs unrelated to your structure of coroutines. The `asyncio` package ships with two different [event loop implementations](https://docs.python.org/3/library/asyncio-eventloop.html#event-loop-implementations).
The default event loop implementation depends on your platform and Python version. For example, on Unix, the default is typically [`SelectorEventLoop`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.SelectorEventLoop), while Windows uses [`ProactorEventLoop`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.ProactorEventLoop) for better subprocess and I/O support.
Third-party event loops are also available. For example, the [uvloop](https://github.com/MagicStack/uvloop) package provides an alternative implementation that promises to be faster than the `asyncio` loops.
### The `asyncio` REPL
Starting with [Python 3.8](https://realpython.com/python38-new-features/), the `asyncio` module includes a specialized interactive shell known as the [asyncio REPL](https://docs.python.org/3/library/asyncio.html#asyncio-cli). This environment allows you to use `await` directly at the top level, without wrapping your code in a call to `asyncio.run()`. This tool facilitates experimenting, debugging, and learning about `asyncio` in Python.
To start the [REPL](https://realpython.com/ref/glossary/repl/), you can run the following command:
Once you get the `>>>` prompt, you can start running asynchronous code there. Consider the example below, where you reuse the code from the previous section:
This example works the same as the one in the previous section. However, instead of running `main()` using `asyncio.run()`, you use `await` directly.
## Common Async I/O Programming Patterns
Async I/O has its own set of possible programming patterns that allow you to write better asynchronous code. In practice, you can *chain coroutines* or use a [queue](https://realpython.com/ref/glossary/queue/) of coroutines. Youâll learn how to use these two patterns in the following sections.
### Coroutine Chaining
A key feature of coroutines is that you can *chain* them together. Remember, a coroutine is awaitable, so another coroutine can await it using the `await` keyword. This makes it easier to break your program into smaller, manageable, and reusable coroutines.
The example below simulates a two-step process that fetches information about a user. The first step fetches the user information, and the second step fetches their published posts:
In this example, you define two major coroutines: `fetch_user()` and `fetch_posts()`. Both simulate a network call with a random delay using `asyncio.sleep()`.
In the `fetch_user()` coroutine, you return a mock user [dictionary](https://realpython.com/python-dicts/). In `fetch_posts()`, you use that dictionary to return a list of mock posts attributed to the user at hand. Random delays simulate real-world asynchronous behavior like network latency.
The coroutine chaining happens in the `get_user_with_posts()`. This coroutine awaits `fetch_user()` and stores the result in the `user` [variable](https://realpython.com/python-variables/). Once the user information is available, itâs passed to `fetch_posts()` to retrieve the posts asynchronously.
In `main()`, you use `asyncio.gather()` to run the chained coroutines by executing `get_user_with_posts()` as many times as the number of user IDs you have.
Hereâs the result of executing the script:
If you sum up the time of all the operations, then this example would take around 7.6 seconds with a synchronous implementation. However, with the asynchronous implementation, it only takes 2.68 seconds.
The pattern, consisting of awaiting one coroutine and passing its result into the next, creates a **coroutine chain**, where each step depends on the previous one. This example mimics a common async workflow where you get one piece of information and use it to get related data.
### Coroutine and Queue Integration
The `asyncio` package provides a few [queue-like classes](https://realpython.com/queue-in-python/#using-asynchronous-queues) that are designed to be similar to [classes](https://realpython.com/python-classes/) in the [`queue`](https://docs.python.org/3/library/queue.html#module-queue) module. In the examples so far, you havenât needed a queue structure. In `chained.py`, each task is performed by a coroutine, which you chain with others to pass data from one to the next.
An alternative approach is to use **producers** that add items to a [queue](https://realpython.com/ref/glossary/queue/). Each producer may add multiple items to the queue at staggered, random, and unannounced times. Then, a group of **consumers** pulls items from the queue as they show up, greedily and without waiting for any other signal.
In this design, thereâs no chaining between producers and consumers. Consumers donât know the number of producers, and vice versa.
It takes an individual producer or consumer a variable amount of time to add and remove items from the queue. The queue serves as a throughput that can communicate with the producers and consumers without them talking to each other directly.
A queue-based version of `chained.py` is shown below:
In this example, the `producer()` function asynchronously fetches mock user data. Each fetched user dictionary is placed into an `asyncio.Queue` object, which shares the data with consumers. After producing all user objects, the producer inserts a [sentinel value](https://en.wikipedia.org/wiki/Sentinel_value)âalso known as a [poison pill](https://realpython.com/queue-in-python/#killing-a-worker-with-the-poison-pill) in this contextâfor each consumer to signal that no more data will be sent, allowing the consumers to shut down cleanly.
The `consumer()` function continuously reads from the queue. If it receives a user dictionary, it simulates fetching that userâs posts, waits a random delay, and prints the results. If it gets the sentinel value, then it breaks the loop and exits.
This decoupling allows multiple consumers to process users concurrently, even while the producer is still generating users, and the queue ensures safe and ordered communication between producers and consumers.
The queue is the communication point between the producers and consumers, enabling a scalable and responsive system.
Hereâs how the code works in practice:
Again, the code runs in only 2.68 seconds, which is more efficient than a synchronous solution. The result is pretty much the same as when you used chained coroutines in the previous section.
## Other Async I/O Features in Python
Pythonâs async I/O features extend beyond the `async def` and `await` constructs. They include other advanced tools that make asynchronous programming more expressive and consistent with regular Python constructs.
In the following sections, youâll explore powerful async features, including async loops and comprehensions, the `async with` statement, and exception groups. These features will help you write cleaner, more readable asynchronous code.
### Async Iterators, Loops, and Comprehensions
Apart from using `async` and `await` to create coroutines, Python also provides the `async for` construct to iterate over an [**asynchronous iterator**](https://realpython.com/ref/glossary/asynchronous-iterator/). An asynchronous iterator allows you to iterate over asynchronously generated data. While the loop runs, it gives control back to the event loop so that other async tasks can run.
A natural extension of this concept is an [**asynchronous generator**](https://realpython.com/ref/glossary/asynchronous-generator/). Hereâs an example that generates powers of two and uses them in a loop and comprehension:
Thereâs a crucial distinction between synchronous and asynchronous generators, loops, and comprehensions. Their asynchronous counterparts donât inherently make iteration concurrent. Instead, they allow the event loop to run other tasks between iterations when you explicitly yield control by using `await`. The iteration itself is still sequential unless you introduce concurrency by using `asyncio.gather()`.
Using `async for` and `async with` is only required when working with asynchronous iterators or context managers, where a regular `for` or `with` would raise errors.
### Async `with` Statements
The [`with` statement](https://realpython.com/python-with-statement/) also has an [asynchronous](https://realpython.com/ref/glossary/asynchronous-programming/) version, `async with`. This construct is quite common in async code, as many [I/O-bound tasks](https://realpython.com/ref/glossary/io-bound-task/) involve setup and teardown phases.
For example, say you need to write a coroutine to check whether some websites are online. To do that, you can use [`aiohttp`](https://docs.aiohttp.org/en/stable/index.html), which is a third-party library that you need to install by running `python -m pip install aiohttp` on your command line.
Hereâs a quick example that implements the required functionality:
In this example, you use `aiohttp` and `asyncio` to perform concurrent [HTTP GET](https://realpython.com/api-integration-in-python/#get) requests to a list of websites. The `check()` coroutine fetches and prints the websiteâs status. The `async with` statement ensures that both `ClientSession` and the individual HTTP response are properly and asynchronously managed by opening and closing them without blocking the event loop.
In this example, using `async with` guarantees that the underlying network resources, including connections and sockets, are correctly released, even if an error occurs.
Finally, `main()` runs the `check()` coroutines concurrently, allowing you to fetch the URLs in parallel without waiting for one to finish before starting the next.
### Other `asyncio` Tools
In addition to `asyncio.run()`, youâve used a few other package-level functions, such as `asyncio.gather()` and `asyncio.get_event_loop()`. You can use [`asyncio.create_task()`](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) to schedule the execution of a coroutine object, followed by the usual call to the `asyncio.run()` function:
This pattern includes a subtle detail you need to be aware of: if you create tasks with `create_task()` but donât await them or wrap them in `gather()`, and your `main()` coroutine finishes, then those manually created tasks will be canceled when the event loop ends. You must await all tasks you want to complete.
The `create_task()` function wraps an awaitable object into a higher-level [`Task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task) object thatâs scheduled to run concurrently on the event loop in the background. In contrast, awaiting a coroutine runs it immediately, pausing the execution of the caller until the awaited coroutine finishes.
The `gather()` function is meant to neatly put a collection of coroutines into a single **future object**. This object represents a placeholder for a result thatâs initially unknown but will be available at some point, typically as the result of asynchronous computations.
If you await `gather()` and specify multiple tasks or coroutines, then the loop will wait for all the tasks to complete. The result of `gather()` will be a list of the results across the inputs:
You probably noticed that `gather()` waits for the entire result of the whole set of coroutines that you pass it. The order of results from `gather()` is deterministic and corresponds to the order of awaitables originally passed to it.
Alternatively, you can loop over `asyncio.as_completed()` to get tasks as they complete. The function returns a synchronous iterator that yields tasks as they finish. Below, the result of `coro([3, 2, 1])` will be available before `coro([10, 5, 2])` is complete, which wasnât the case with the `gather()` function:
In this example, the `main()` function uses `asyncio.as_completed()`, which yields tasks in the order they complete, not in the order they were started. As the program loops through the tasks, it awaits them, allowing the results to be available immediately upon completion.
As a result, the faster task (`task1`) finishes first and its result is printed earlier, while the longer task (`task2`) completes and prints afterward. The `as_completed()` function is useful when you need to handle tasks dynamically as they finish, which improves responsiveness in concurrent workflows.
### Async Exception Handling
Starting with [Python 3.11](https://realpython.com/python311-new-features/), you can use the [`ExceptionGroup`](https://realpython.com/python311-exception-groups/) class to handle multiple unrelated exceptions that may occur concurrently. This is especially useful when running multiple coroutines that can raise different exceptions. Additionally, the new `except*` syntax helps you gracefully deal with several errors at once.
Hereâs a quick demo of how to use this class in asynchronous code:
In this example, you have three coroutines that raise three different types of [exceptions](https://realpython.com/python-built-in-exceptions/). In the `main()` function, you call `gather()` with the coroutines as arguments. You also set the `return_exceptions` argument to `True` so that you can grab the exceptions if they occur.
Next, you use a list comprehension to store the exceptions in a new list. If the list contains at least one exception, then you create an `ExceptionGroup` for them.
To handle this exception group, you can use the following code:
In this code, you wrap the call to `asyncio.run()` in a [`try`](https://realpython.com/ref/keywords/try/) block. Then, you use the `except*` syntax to catch the expected exception separately. In each case, you print an error message to the screen.
## Async I/O in Context
Now that youâve seen a healthy dose of asynchronous code, take a moment to step back and consider when async I/O is the ideal choiceâand how to evaluate whether itâs the right fit or if another concurrency model might be better.
### When to Use Async I/O
Using `async def` for functions that perform blocking operationsâsuch as standard file I/O or synchronous network requestsâwill block the entire event loop, negate the benefits of async I/O, and potentially reduce your programâs efficiency. Only use `async def` functions for [non-blocking operations](https://realpython.com/ref/glossary/non-blocking-operation/).
The battle between async I/O and multiprocessing isnât a real battle. You can use both models [in concert](https://youtu.be/0kXaLh8Fz3k?t=10m30s) if you want. In practice, multiprocessing should be the right choice if you have multiple CPU-bound tasks.
The contest between async I/O and threading is more direct. Threading isnât simple, and even in cases where threading seems easy to implement, it can still lead to hard-to-trace bugs due to [race conditions](https://realpython.com/python-thread-lock/#race-conditions) and memory usage, among other things.
Threading also tends to scale less elegantly than async I/O because threads are a system resource with a finite availability. Creating thousands of threads will fail on many machines or can slow down your code. In contrast, creating thousands of async I/O tasks is completely feasible.
Async I/O shines when you have multiple I/O-bound tasks that would otherwise be dominated by blocking wait time, such as:
- **Network I/O**, whether your program is acting as the server or the client
- **Serverless designs**, such as a peer-to-peer, multi-user network like a group chat
- **Read/write operations** where you want to mimic a [fire-and-forget](https://en.wikipedia.org/wiki/Fire-and-forget) style approach without worrying about holding a lock on the resource
The biggest reason not to use async I/O is that `await` only supports a specific set of objects that define a particular set of methods. For example, if you want to do async read operations on a certain [database management system (DBMS)](https://en.wikipedia.org/wiki/Database#Database_management_system), then youâll need to find a Python wrapper for that DBMS that supports the `async` and `await` syntax.
### Libraries Supporting Async I/O
Youâll find several high-quality third-party libraries and frameworks that support or are built on top of `asyncio` in Python, including tools for web servers, databases, networking, testing, and more. Here are some of the most notable:
- **Web frameworks:**
- [FastAPI](https://fastapi.tiangolo.com/): Modern async web framework for building [web APIs](https://realpython.com/python-api/).
- [Starlette](https://www.starlette.io/): Lightweight [asynchronous server gateway interface (ASGI)](https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface) framework for building high-performance async web apps.
- [Sanic](https://sanic.dev/): Async web framework built for speed using `asyncio`.
- [Quart](https://github.com/pallets/quart): Async web microframework with the same API as [Flask](https://realpython.com/flask-project/).
- [Tornado](https://github.com/tornadoweb/tornado): Performant web framework and asynchronous networking library.
- **ASGI servers:**
- [uvicorn](https://www.uvicorn.org/): Fast ASGI web server.
- [Hypercorn](https://pypi.org/project/Hypercorn/): ASGI server supporting several protocols and configuration options.
- **Networking tools:**
- [aiohttp](https://docs.aiohttp.org/): HTTP client and server implementation using `asyncio`.
- [HTTPX](https://www.python-httpx.org/): Fully featured async and sync HTTP client.
- [websockets](https://websockets.readthedocs.io/): Library for building WebSocket servers and clients with `asyncio`.
- [aiosmtplib](https://aiosmtplib.readthedocs.io/): Async SMTP client for [sending emails](https://realpython.com/python-send-email/).
- **Database tools:**
- [Databases](https://www.encode.io/databases/): Async database access layer compatible with [SQLAlchemy](https://realpython.com/python-sqlite-sqlalchemy/) core.
- [Tortoise ORM](https://tortoise.github.io/): Lightweight async object-relational mapper (ORM).
- [Gino](https://python-gino.org/): Async ORM built on SQLAlchemy core for [PostgreSQL](https://realpython.com/python-sql-libraries/#postgresql).
- [Motor](https://motor.readthedocs.io/): Async [MongoDB](https://realpython.com/introduction-to-mongodb-and-python/) driver built on `asyncio`.
- **Utility libraries:**
- [aiofiles](https://github.com/Tinche/aiofiles): Wraps Pythonâs file API for use with `async` and `await`.
- [aiocache](https://github.com/aio-libs/aiocache): Async caching library supporting [Redis](https://realpython.com/python-redis/) and Memcached.
- [APScheduler](https://github.com/agronholm/apscheduler): A task scheduler with support for async jobs.
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/): Adds support for testing async functions using [pytest](https://realpython.com/pytest-python-testing/).
These libraries and frameworks help you write performant async Python applications. Whether youâre building a web server, fetching data over the network, or accessing a database, `asyncio` tools like these give you the power to handle many tasks concurrently with minimal overhead.
## Conclusion
Youâve gained a solid understanding of Pythonâs `asyncio` library and the `async` and `await` syntax, learning how asynchronous programming enables efficient management of multiple I/O-bound tasks within a single thread.
Along the way, you explored the differences between concurrency, parallelism, threading, multiprocessing, and asynchronous I/O. You also worked through practical examples using coroutines, event loops, chaining, and queue-based concurrency. On top of that, you learned about advanced `asyncio` features, including async context managers, async iterators, comprehensions, and how to leverage third-party async libraries.
Mastering `asyncio` is essential when building scalable network servers, web APIs, or applications that perform many simultaneous I/O-bound operations.
**In this tutorial, youâve learned how to:**
- **Distinguish** between concurrency models and identify when to use **`asyncio`** for I/O-bound tasks
- **Write**, **run**, and **chain coroutines** using `async def` and `await`
- **Manage the event loop** and schedule multiple tasks with `asyncio.run()`, `gather()`, and `create_task()`
- Implement async patterns like **coroutine chaining** and **async queues** for producerâconsumer workflows
- **Use advanced async features** such as `async for` and `async with`, and integrate with **third-party async libraries**
With these skills, youâre ready to build high-performance, modern Python applications that can handle many operations asynchronously.
## Frequently Asked Questions
Now that you have some experience with `asyncio` in Python, you can use the questions and answers below to check your understanding and recap what youâve learned.
These FAQs are related to the most important concepts youâve covered in this tutorial. Click the *Show/Hide* toggle beside each question to reveal the answer.
You use `asyncio` to write concurrent code with the `async` and `await` keywords, allowing you to efficiently manage multiple I/O-bound tasks in a single thread without blocking your program.
You typically get better performance from `asyncio` for I/O-bound work because it avoids the overhead and complexity of threads. This allows thousands of tasks to run concurrently without the limitations of Pythonâs GIL.
Use `asyncio` when your program spends a significant amount of time waiting on I/O-bound operationsâsuch as network requests or file accessâand you want to run many of these tasks concurrently and efficiently.
You define a coroutine using the `async def` syntax. To run it, either pass it to `asyncio.run()` or schedule it as a task with `asyncio.create_task()`.
You rely on the event loop to manage the scheduling and execution of your coroutines, giving each one a chance to run whenever it awaits or completes an I/O-bound operation.
***Take the Quiz:*** Test your knowledge with our interactive âPython's asyncio: A Hands-On Walkthroughâ quiz. Youâll receive a score upon completion to help you track your learning progress:
***
[](https://realpython.com/quizzes/async-io-python/) |
| Shard | 71 (laksa) |
| Root Hash | 13351397557425671 |
| Unparsed URL | com,realpython!/async-io-python/ s443 |