Asyncio provides asynchronous programming in Python with coroutines.
It is exciting, new, and can be deeply frustrating to beginners. The reason is because of a series of common errors made when getting started with coroutines and the asyncio API.
In this tutorial, you will discover the most common errors encountered by beginners in asyncio in Python.
Let’s get started.
Python Asyncio Common Errors
Asyncio can be frustrating for beginners.
Coroutines look like functions but aren’t used like functions. It causes a lot of confusion.
There are common errors experienced by beginners when getting started with asyncio in Python.
They are:
- Trying to run coroutines by calling them.
- Not letting coroutines run in the event loop.
- Using the asyncio low-level API.
- Exiting the main coroutine too early.
- Assuming race conditions and deadlocks are impossible.
Let’s take a closer look at each in turn.
Run loops using all CPUs, download your FREE book to learn how.
Error 1: Trying to Run Coroutines by Calling Them
The most common error encountered by beginners to asyncio is calling a coroutine like a function.
For example, we can define a coroutine using the “async def” expression:
1 2 3 |
# custom coroutine async def custom_coro(): print('hi there') |
The beginner will then attempt to call this coroutine like a function and expect the print message to be reported.
For example:
1 2 3 |
... # error attempt at calling a coroutine like a function custom_coro() |
Calling a coroutine like a function will not execute the body of the coroutine.
Instead, it will create a coroutine object.
You can learn more about coroutines in the tutorial:
This object can then be awaited within the asyncio runtime, e.g. the event loop.
We can start the event loop to run the coroutine using the asyncio.run() function.
For example:
1 2 3 |
... # run a coroutine asyncio.run(custom_coro()) |
Alternatively, we can suspend the current coroutine and schedule the other coroutine using the “await” expression.
For example:
1 2 3 |
... # schedule a coroutine await custom_coro() |
You can learn more about running coroutines in the tutorial:
Error 2: Not Letting Coroutines Run in the Event Loop
If a coroutine is not run, you will get a runtime warning as follows:
1 |
sys:1: RuntimeWarning: coroutine 'custom_coro' was never awaited |
This will happen if you create a coroutine object but do not schedule it for execution within the asyncio event loop.
For example, you may attempt to call a coroutine from a regular Python program:
1 2 3 |
... # attempt to call the coroutine custom_coro() |
This will not call the coroutine.
Instead, it will create a coroutine object.
For example:
1 2 3 |
... # create a coroutine object coro = custom_coro() |
If you do not allow this coroutine to run, you will get a runtime error.
You can let the coroutine run, as we saw in the previous section, by starting the asyncio event loop and passing it to the coroutine object.
For example:
1 2 3 4 5 |
... # create a coroutine object coro = custom_coro() # run a coroutine asyncio.run(coro) |
Or, on one line in a compound statement:
1 2 3 |
... # run a coroutine asyncio.run(custom_coro()) |
You can learn more about running coroutines in the tutorial:
If you get this error within an asyncio program, it is because you have created a coroutine and have not scheduled it for execution.
This can be achieved using the await expression.
For example:
1 2 3 4 5 |
... # create a coroutine object coro = custom_coro() # suspend and allow the other coroutine to run await coro |
Or, you can schedule it to run independently as a task.
For example:
1 2 3 4 5 |
... # create a coroutine object coro = custom_coro() # schedule the coro to run as a task interdependently task = asyncio.create_task(coro) |
You can learn more about creating tasks in the tutorial:
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
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.
Error 3: Using the Low-Level Asyncio API
A big problem with beginners is that they use the wrong asyncio API.
This is common for a number of reasons.
- The API has changed a lot with recent versions of Python.
- The API docs page makes things confusing, showing both APIs.
- Examples elsewhere on the web mix up using the different APIs.
Using the wrong API makes things more verbose (e.g. more code), more difficult, and way less understandable.
Asyncio offers two APIs.
- High-level API for application developers (us)
- Low-level API for framework and library developers (not us)
The lower-level API provides the foundation for the high-level API and includes the internals of the event loop, transport protocols, policies, and more.
… there are low-level APIs for library and framework developers
— asyncio — Asynchronous I/O
We should almost always stick to the high-level API.
We absolutely must stick to the high-level API when getting started.
We may dip into the low-level API to achieve specific outcomes on occasion.
If you start getting a handle on the event loop or use a “loop” variable to do things, you are doing it wrong.
I am not saying don’t learn the low-level API.
Go for it. It’s great.
Just don’t start there.
Drive asyncio via the high-level API for a while. Develop some programs. Get comfortable with asynchronous programming and running coroutines at will.
Then later, dip in and have a look around.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Error 4: Exiting the Main Coroutine Too Early
A major point of confusion in asyncio programs is not giving tasks enough time to complete.
We can schedule many coroutines to run independently within an asyncio program via the asyncio.create_task() method.
The main coroutine, the entry point for the asyncio program, can then carry on with other activities.
If the main coroutine exits, then the asyncio program will terminate.
The program will terminate even if there are one or many coroutines running independently as tasks.
You can learn more about how to exit an asyncio program in the tutorial:
This can catch you off guard.
You may issue many tasks and then allow the main coroutine to resume, expecting all issued tasks to complete in their own time.
Instead, if the main coroutine has nothing else to do, it should wait on the remaining tasks.
This can be achieved by first getting a set of all running tasks via the asyncio.all_tasks() function, removing itself from this set, then waiting on the remaining tasks via the asyncio.wait() function.
For example:
1 2 3 4 5 6 7 8 9 |
... # get a set of all running tasks all_tasks = asyncio.all_tasks() # get the current tasks current_task = asyncio.current_task() # remove the current task from the list of all tasks all_tasks.remove(current_task) # suspend until all tasks are completed await asyncio.wait(all_tasks) |
You can learn more about waiting on all independent tasks in the tutorial:
Error 5: Assuming Race Conditions and Deadlocks are Impossible
Concurrent programming has the hazard of concurrency-specific failure modes.
This includes problems such as race conditions and deadlocks.
A race condition involves two or more units of concurrency executing the same critical section at the same time and leaving a resource or data in an inconsistent or unexpected state. This can lead to data corruption and data loss.
A deadlock is when a unit of concurrency waits for a condition that can never occur, such as for a resource to become available.
Many Python developers believe these problems are not possible with coroutines in asyncio.
The reason is that only one coroutine can run within the event loop at any one time.
It is true that only one coroutine can run at a time.
The problem is, coroutines can suspend and resume and may do so while using a shared resource or shared variable.
Without protecting critical sections, race conditions can occur in asyncio programs.
You can see an example in the tutorial:
Without careful management of synchronization primitives, deadlocks can occur in asyncio programs.
You can see an example in the tutorial:
As such, it is important that asyncio programs are created to ensure coroutine-safety, a concept similar to thread safety and process-safety, applied to coroutines.
You can learn more about coroutine safety in the tutorial:
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- 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 Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Takeaways
You now know the most common errors encountered by beginners to asyncio in Python and how to avoid them.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Media Studio Hong Kong on Unsplash
Do you have any questions?