Python asyncio is new and powerful, yet confusing to many Python developers.
In this tutorial, you will discover when to use asyncio in your Python programs.
Let’s get started.
Table of Contents
When to Use Asyncio
Asyncio refers to asynchronous programming with coroutines in Python.
It involves changes to the Python programming language to support coroutines, with new types and expressions such as async def, async with, async for, and await.
It also involves a module called “asyncio” that provides functions and infrastructure for developing programs using coroutines.
Asyncio, broadly, is new, popular, much discussed, and exciting.
Nevertheless, there is a lot of confusion over when it should be adopted in a project.
When should we use asyncio in Python?
Run your loops using all CPUs, download my FREE book to learn how.
6 Reasons to Use Asyncio in Python
There are six reasons to use asyncio in a Python project.
- Use asyncio in order to adopt coroutines in your program.
- Use coroutines as an alternative to threads and processes.
- Achieve scalability offered by coroutines compared to threads.
- Use asyncio in order to use the asynchronous programming paradigm.
- Use asyncio in order to use non-blocking I/O.
- Use asyncio because someone else made the decision for you.
- Use asyncio because the project you have joined is already using it.
- Use asyncio because you want to learn more about it.
Ideally, we would choose a reason that is defended in the context of the requirements of the project.
Sometimes we have control over the function and non-functional requirements and other times not. In the cases we do, we may choose to use asyncio for one of the reasons listed above. In the cases we don’t, we may be led to choose asyncio in order to deliver a program that solves a specific problem.
Do you know another reason why you might use asyncio?
Let me know in the comments below.
Next, let’s take a closer look at these three high-level reasons to use asyncio.
Confused by the asyncio module API?
Download my FREE PDF cheat sheet
Reason 1: To Use Coroutines
We may choose to use asyncio because we want to use coroutines.
We may want to use coroutines because we can have many more concurrent coroutines in our program than concurrent threads.
Coroutines are another unit of concurrency, like threads and processes.
Thread-based concurrency is provided by the threading module and is supported by the underlying operating system. It is suited to blocking I/O tasks such reading and writing from files, sockets, and devices.
Process-based concurrency is provided by the multiprocessing module and is also supported by the underlying operating system, like threads. It is suited to CPU-bound tasks that do not require much inter-process communication, such as compute tasks.
Coroutines are an alternative that is provided by the Python language and runtime (standard interpreter) and further supported by the asyncio module. They are suited to non-blocking I/O with subprocesses and sockets, however, blocking I/O and CPU-bound tasks can be used in a simulated non-blocking manner using threads and processes under the covers.
This last point is subtle and key. Although we can choose to use coroutines for the capability for which they were introduced into Python, non-blocking, we may in fact use them with any tasks. Any program written with threads or processes can be rewritten or instead written using coroutines if we so desire.
Threads and processes achieve multitasking via the operating system that chooses which threads and processes should run, when, and for how long. The operating switches between threads and processes rapidly, suspending those that are not running and resuming those granted time to run. This is called preemptive multitasking.
Coroutines in Python provide an alternative type of multitasking called cooperating multitasking.
A coroutine is a subroutine (function) that can be suspended and resumed. It is suspended by the await expression and resumed once the await expression is resolved.
coroutine: Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points.— Python Glossary
This allows coroutines to cooperate by design, choosing how and when to suspend their execution.
It is an alternate, interesting, and powerful approach to concurrency, different from thread-based and process-based concurrency.
This alone may make it a reason to adopt it for a project.
Another key aspect of coroutines is that they are lightweight.
They are more lightweight than threads. This means they are faster to start and use less memory. Essentially a coroutine is a special type of function, whereas a thread is represented by a Python object and is associated with a thread in the operating system with which the object must interact.
As such, we may have thousands of threads in a Python program, but we could easily have tens or hundreds of thousands of coroutines all in one thread.
We may choose coroutines for their scalability.
Coroutines let you have a very large number of seemingly simultaneous functions in your Python programs.— Page 267, Effective Python, 2019.
We may choose to use asyncio because we want to use coroutines in our program, and that is a defensible reason.
You can learn more about coroutines in the tutorial:
Free Python Asyncio Course
Download my asyncio API cheat sheet and as a bonus you will get FREE access to my 7-day email course on asyncio.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
Reason 2: To Use Asynchronous Programming
We may choose to use asyncio because we want to use asynchronous programming in our program.
That is, we want to develop a Python program that uses the asynchronous programming paradigm.
Asynchronous means not at the same time, as opposed to synchronous or at the same time.
asynchronous: not simultaneous or concurrent in time— Merriam-Webster Dictionary
When programming, asynchronous means that the action is requested, although not performed at the time of the request. It is performed later.
For example, we can make an asynchronous function call. This will issue the request to make the function call and will not wait around for the call to complete. We can choose to check on the status or result of the function call later.
The function call will happen somehow and at some time, in the background, and the program can perform other tasks or respond to other events.
This may require specific programming patterns.
For example, issuing an asynchronous function call often results in some handle on the request that the caller can use to check on the status of the call or get results. This is often called a future.
- Future: A handle on an asynchronous function call allowing the status of the call to be checked and results to be retrieved.
Developing programs focused on asynchronous function calls and asynchronous tasks that make use of facilitating programming patterns, like Futures, is referred to as asynchronous programming.
- Asynchronous Programming: The use of asynchronous techniques, such as issuing asynchronous tasks or function calls.
Broadly, asynchronous programming in Python refers to making requests and not blocking to wait for them to complete.
We can implement asynchronous programming in Python in various ways, although a few are most relevant for Python concurrency.
For example, the Pool and ThreadPool classes in the multiprocessing.pool module support asynchronous task execution with threads and processes, as do the ThreadPoolExecutor and ProcessPoolExecutor in the concurrent.futures module.
The concurrent.futures module provides a high-level interface for asynchronously executing callables. The asynchronous execution can be performed with threads, using ThreadPoolExecutor, or separate processes, using ProcessPoolExecutor.— concurrent.futures — Launching parallel tasks
These examples allow asynchronous programming to be added to an existing Python program.
Asynchronous programming often means going all in and designing the program around the concept of asynchronous function calls and tasks.
Although there are other ways to achieve elements of asynchronous programming, full asynchronous programming in Python requires the use of coroutines and the asyncio module.
It is a Python library that allows us to run code using an asynchronous programming model.— Page 3, Python Concurrency with asyncio, 2022.
We may choose to use asyncio because we want to use the asynchronous programming module in our program, and that is a defensible reason.
To be crystal clear, this reason is independent of using non-blocking I/O. Asynchronous programming can be used independently of non-blocking I/O.
As we saw previously, coroutines can execute non-blocking I/O asynchronously, but the asyncio module also provides the facility for executing blocking I/O and CPU-bound tasks in an asynchronous manner, simulating non-blocking under the covers via threads and processes.
You can learn more about asynchronous programming in Python in the tutorial:
Overwheled by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Reason 3: To Use Non-Blocking I/O
We may choose to use asyncio because we want or require non-blocking I/O in our program.
Input/Output or I/O for short means reading or writing from a resource.
Common examples include:
- Hard disk drive: Reading, writing, appending, renaming, deleting, etc. files.
- Peripherals: mouse, keyboard, screen, printer, serial, camera, etc.
- Internet: Downloading and uploading files, getting a webpage, querying RSS, etc.
- Database: Select, update, delete, etc. SQL queries.
- Email: Send mail, receive mail, query inbox, etc.
These operations are slow, compared to calculating things with the CPU.
The common way these operations are implemented in programs is to make the read or write request and then wait for the data to be sent or received.
As such, these operations are commonly referred to as blocking I/O tasks.
The operating system can see that the calling thread is blocked and will context switch to another thread that will make use of the CPU.
This means that the blocking call does not slow down the entire system. But it does halt or block the thread or program making the blocking call.
You can learn more about blocking calls in the tutorial:
Non-blocking I/O is an alternative to blocking I/O.
It requires support in the underlying operating system, just like blocking I/O, and all modern operating systems provide support for some form of non-blocking I/O.
Non-blocking I/O allows read and write calls to be made as asynchronous requests.
The operating system will handle the request and notify the calling program when the results are available.
- Non-blocking I/O: Performing I/O operations via asynchronous requests and responses, rather than waiting for operations to complete.
As such, we can see how non-blocking I/O is related to asynchronous programming. In fact, we use non-blocking I/O via asynchronous programming or non-blocking I/O is implemented via asynchronous programming.
The combination of non-blocking I/O with asynchronous programming is so common that it is commonly referred to by the shorthand of asynchronous I/O.
- Asynchronous I/O: A shorthand that refers to combining asynchronous programming with non-blocking I/O.
The asyncio module in Python was added specifically to add support for non-blocking I/O with subprocesses (e.g. executing commands on the operating system) and with streams (e.g. TCP socket programming) to the Python standard library.
We could simulate non-blocking I/O using threads and the asynchronous programming capability provided by Python thread pools or thread pool executors.
The asyncio module provides first-class asynchronous programming for non-blocking I/O via coroutines, event loops, and objects to represent non-blocking subprocesses and streams.
We may choose to use asyncio because we want to use asynchronous I/O in our program, and that is a defensible reason.
Reason 4: Someone Else Made the Decision
We may use asyncio on a Python project because someone else told us to.
We don’t always have full control over the projects we work on.
It is common to start a new job, new role, or new project and be told by the line manager or lead architect of specific design and technology decisions.
Using asyncio may be one of these decisions.
It may be a reasoned decision. It may, and often is not, e.g. it is common to use cool new tech on a new project “just because“, creating all kinds of maintenance debt. But that is another topic.
This is a less often considered reason to use asyncio, and a very strong motivator to understand and get good at using it quickly.
Someone other than you may choose to use asyncio on a project. Therefore, you must use asyncio.
As simple as that.
Reason 5: Project Already Used Asyncio
We may use asyncio on a project because the project is already using it.
There are more programming jobs for maintenance than green field development. Let’s call them brownfields.
Someone else or some other team developed the program and now you are part of the effort to maintain it into the future.
You must know and use whatever modules and libraries are used by the project already.
As such, if the project uses asyncio, you must use asyncio.
As with the previous section, this is less of a reason and more of a forced situation.
You must use asyncio, rather than you choose to use asyncio.
A related example might be the case of a solution to a problem that uses asyncio that you wish to adopt.
- Perhaps you need to use a third-party API and the code examples use asyncio.
- Perhaps you need to integrate an existing open-source solution that uses asyncio.
- Perhaps you stumble across some code snippets that do what you need, yet they use asyncio.
For lack of alternate solutions, asyncio may be thrust upon you by your choice of solution.
Reason 6: You Want to Learn More About it
We may choose asyncio for our Python project to learn more about.
You may scoff, “what about the requirements?”
In reality, most decisions are made this way. Just because we want to. Emotional.
We become developers in the first place, in the face of alternatives, because we want to. No justification is needed, other than the story we tell post hoc.
The same with new tech.
It is one thing to read about the benefits of asyncio and how coroutines work.
It is another to use them in a project, “in anger” so to speak.
Using asyncio in a project will make its workings concrete for you.
It may be a short-lived project, like a small one-off script.
It probably won’t be a large multi-year long-lived project.
Don’t underestimate this reason.
You may choose to adopt asyncio just because you want to try it out and it can be a defensible reason.
How else would you learn about it at such depth?
You can learn more about the important emotional reasons to dive into new methods in the guide:
When to Not Use Asyncio
We have spent a lot of time on reasons why we should use asyncio.
It is probably a good idea to spend at least a moment on why we should not use it.
One reason to not use asyncio is that you cannot defend its use using one of the reasons above.
This is not foolproof. There may be other reasons to use it, not listed above.
But, if you pick a reason to use asyncio and the reason feels thin or full of holes for your specific case. Perhaps asyncio is not the right solution.
I think the major reason to not use asyncio is that it does not deliver the benefit that you think it does.
There are many misconceptions about Python concurrency, especially around asyncio.
- Asyncio will work around the global interpreter lock.
- Asyncio is faster than threads.
- Asyncio avoids the need for mutex locks and other synchronization primitives.
- Asyncio is easier to use than threads.
These are all false.
Only a single coroutine can run at a time by design, they cooperate to execute. This is just like threads under the GIL. In fact, the GIL is an orthogonal concern and probably irrelevant in most cases when using asyncio.
Any program you can write with asyncio, you can write with threads and it will probably be as fast or faster. It will also probably be simpler and easier to read and interpret by fellow developers.
Any concurrency failure mode you might expect with threads, you can encounter with coroutines. You must make coroutines safe from deadlocks and race conditions, just like threads.
Another reason to not use asyncio is that you don’t like asynchronous programming.
It is different from procedural, object-oriented, and functional programming, and some developers just don’t like it.
No problem. If you don’t like it, don’t use it. It’s a fair reason.
You can achieve the same effect in many ways, notably by sprinkling a few asynchronous calls in via thread or process executors as needed.
This section provides additional resources that you may find helpful.
- Python Asyncio Jump-Start, Jason Brownlee, 2022 (my book).
- Python Asyncio Interview Questions
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
You now know when to use asyncio in your Python projects.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Dhiva Krishna on Unsplash
Leave a Reply