You can exit the asyncio event loop by returning from the main coroutine used as the entry point for the asyncio program.
In this tutorial, you will discover how to exit the asyncio event loop in Python.
Let’s get started.
Need to Exit the Event Loop
Asyncio programs are executed by an event loop.
The event loop is responsible for running tasks, executing callbacks and managing non-blocking I/O.
The event loop is the core of every asyncio application. Event loops run asynchronous tasks and callbacks, perform network IO operations, and run subprocesses.
— Asyncio Event Loop
You can learn more about the asyncio event loop in the tutorial:
We execute asyncio programs using the high-level API via the asyncio.run() function.
This function takes a coroutine as an argument which is used as the entry point to the asyncio program.
For example:
1 2 3 |
... # run an asyncio program asyncio.run(main()) |
You can learn more about how to run asyncio programs in the tutorial:
Running the event loop is blocking, this means it will block the calling thread until the event loop is terminated.
This raises the question, how do we exit the asyncio event loop?
We may need to exit the asyncio event loop from within the asyncio program for many reasons, such as:
- User request.
- Failure of a required resource.
- Early completion of tasks.
How can an asyncio program exit the event loop?
It also raises related questions, such as:
- Does a running task prevent the asyncio event loop from exiting normally?
- Does a running subprocess prevent the asyncio event loop from exiting normally?
We can explore these questions with worked examples.
Run loops using all CPUs, download your FREE book to learn how.
How to Exit the Asyncio Event Loop
An asyncio event loop can be exited by returning from the main coroutine.
We may refer to the coroutine that is passed to asyncio.run() to run the asyncio event loop as the main coroutine, for lack of a better name.
When this coroutine exits, the event loop will terminate.
This is the normal way for an asyncio program to end.
There are three ways this may occur, they are:
- The main coroutine completes, e.g. reaches the end and returns,
- The main coroutine returns early, e.g. the return expression.
- The main coroutine raises an unhandled exception, e.g. directly or indirectly.
The asyncio event loop does provide methods for stopping the event loop. These are the close() method and the stop() method.
The close() method can only be called once all tasks have stopped running otherwise a RuntimeError will be raised. It cannot be called safely from a running coroutine directly. The stop() method will schedule an attempt to close the event loop if run_forever() was called, but it too requires that all tasks have been completed otherwise it will raise a RuntimeError.
These are in a low-level API and are generally not intended for application developers, instead, they are reserved for framework developers. You may be able to trigger a close using these methods, e.g. via another thread, but they are not recommended.
Generally, these methods are intended to be called from outside the event loop, e.g. by a managing thread or process.
Now that we know how to exit the asyncio event loop, let’s look at some worked examples.
Example of Exiting the Event Loop
The recommended way to exit the asyncio event loop is to exit the main coroutine.
In this section, we will explore the different common ways of exiting the main coroutine.
Exit the Event Loop After the Main Coroutine Finishes
We can explore the normal case of closing the asyncio event loop by letting the main coroutine run to completion.
In this example, the main coroutine will schedule a task coroutine and wait for it to complete. The task will report a message, sleep for two seconds, then report a final message. Once the task is complete, the main coroutine will resume and report a final message for exiting.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# SuperFastPython.com # example of exiting the event loop normally import asyncio # task coroutine async def work(): # report a message print('Task starting') # simulate work await asyncio.sleep(2) # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment await task # report a message print('Main done') # run the asyncio program asyncio.run(main()) |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task. It then awaits the task and suspends it until the task is completed.
The task runs, reports a message, sleeps for 2 seconds, then reports a final message.
The main() coroutine resumes and reports a final message before ending normally.
The asyncio event loop is then terminated and the main thread is closed.
This highlights how we can exit the asyncio event loop normally by completing the main coroutine.
1 2 3 |
Task starting Task done Main done |
Exit the Event Loop With an Early Return
We can explore how to exit the asyncio event loop with an early return statement in the main coroutine.
In this example, the main coroutine will schedule an independent task, suspend a moment to allow the task to begin executing, then issue an early return before the independent task has had a chance to complete.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# SuperFastPython.com # forcing the exit of the event loop import asyncio # task coroutine async def work(): # report a message print('Task starting') # simulate work await asyncio.sleep(2) # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment await asyncio.sleep(1) # exit the event loop return # report a message print('Main done') # run the asyncio program asyncio.run(main()) |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task.
It then sleeps for one second, suspending and allowing the task to run.
The task begins running, reporting a message, and sleeping.
The main() coroutine resumes and returns.
This terminates the asyncio event loop and the main thread. The running task is terminated along with the event loop and is not given an opportunity to complete.
This highlights how returning from the main coroutine at any point will terminate the asyncio event loop.
1 |
Task starting |
Exit the Event Loop With an Exception
We can explore how to exit the asyncio event loop by raising an unhandled exception.
This will terminate the main coroutine and cause it to return, terminating the asyncio event loop.
In this example, the main coroutine will schedule an independent task and suspend a moment to allow the task to begin executing. It will then raise an exception that is not handled, causing the main coroutine to terminate and in turn close the event loop.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# SuperFastPython.com # forcing the exit of the event loop import asyncio # task coroutine async def work(): # report a message print('Task starting') # simulate work await asyncio.sleep(2) # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment await asyncio.sleep(1) # exit the event loop raise Exception('Stop this thing') # report a message print('Main done') # run the asyncio program asyncio.run(main()) |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task.
It then sleeps for one second, suspending and allowing the task to run.
The task begins running, reporting a message, and sleeping.
The main() coroutine resumes and raises an unhandled exception.
This exits the main() coroutine and terminates the asyncio event loop. The running task is not given an opportunity to complete.
This highlights how we may exit the asyncio event loop by raising an unhandled exception.
1 2 3 4 |
Task starting Traceback (most recent call last): ... Exception: Stop this thing |
The main coroutine does not have to raise an exception directly in order to terminate.
An exception can be raised in a called function or awaited coroutine.
If unheralded, the exception can propagate back to the main coroutine and in turn terminate the event loop.
The example below demonstrates this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# SuperFastPython.com # forcing the exit of the event loop import asyncio # task coroutine async def work(): # report a message print('Task starting') # simulate work await asyncio.sleep(2) # exit the event loop raise Exception('Stop this thing') # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment await task # report a message print('Main done') # run the asyncio program asyncio.run(main()) |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task. It then awaits the task and suspends it until the task is completed.
The task runs, reports a message, sleeps for 2 seconds, then raises an exception.
The exception propagates from the work() coroutine to the main() coroutine and then exits the main() coroutine, terminating the asyncio event loop.
This highlights that we may exit the asyncio event loop by allowing exceptions to bubble up from coroutines to the main coroutine.
1 2 3 4 |
Task starting Traceback (most recent call last): ... Exception: Stop this thing |
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.
Cannot Directly Stop the Event Loop While a Task is Running
We cannot directly close or stop the event loop from within the event loop.
The low-level event loop provides close() and stop() methods intended for stopping an asyncio event loop from the outside, such as by the owning thread or process.
These methods generally cannot be called directly from coroutines and tasks running in the event loop.
We can explore this with some worked examples.
Cannot Call close() From Within a Coroutine
We can explore calling the close() method from the main coroutine.
In this example, we will schedule a task in the background, suspend it for a moment to allow the task to start, then call the close() method on the currently running event loop.
We expect this to fail with a RuntimeError exception as we cannot close the event loop while tasks are executing.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# SuperFastPython.com # forcing the exit of the event loop import asyncio # task coroutine async def work(): # report a message print('Task starting') # simulate work await asyncio.sleep(2) # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment await asyncio.sleep(1) # get the event loop loop = asyncio.get_running_loop() # schedule a stop to the event loop loop.close() # report a message print('Main done') # run the asyncio program asyncio.run(main()) |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task.
It then sleeps for one second, suspending and allowing the task to run.
The main() coroutine then gets access to the running event loop and calls the close() method.
This fails with a RuntimeError exception, as we expected, indicating that we cannot close a running event loop.
1 2 3 4 |
Task starting Traceback (most recent call last): ... RuntimeError: Cannot close a running event loop |
Cannot Call stop() From Within a Coroutine
We can explore calling the stop() method from the main coroutine.
This method is intended to be called on an event loop that is running forever, via the run_forever() method.
In this example, we will schedule a task in the background, suspend it for a moment to allow the task to start, then call the stop() method on the currently running event loop.
We expect this to fail with a RuntimeError exception as we cannot stop the event loop while tasks are executing.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# SuperFastPython.com # forcing the exit of the event loop import asyncio # task coroutine async def work(): # report a message print('Task starting') # simulate work await asyncio.sleep(2) # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment await asyncio.sleep(1) # get the event loop loop = asyncio.get_running_loop() # schedule a stop to the event loop loop.stop() # report a message print('Main done') # run the asyncio program asyncio.run(main()) |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task.
It then sleeps for one second, suspending and allowing the task to run.
The main() coroutine then gets access to the running event loop and calls the stop() method.
This fails with a RuntimeError exception, as we expected, indicating the event loop was stopped while tasks were still incomplete.
1 2 3 4 5 |
Task starting Main done Traceback (most recent call last): ... RuntimeError: Event loop stopped before Future completed. |
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Does a Task Stop the Event Loop Exiting
We can explore if the event loop will stop if an independent task is still executing.
This can be achieved by scheduling the task, allowing it to start running, then having the main coroutine return.
This will either block, allowing the independent task to complete, or return immediately, terminating the event loop and the running task.
The example below explores this scenario.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# SuperFastPython.com # test if a task stops the event loop from exiting import asyncio # task coroutine async def work(): # report a message print('Task starting') # simulate work await asyncio.sleep(2) # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment await asyncio.sleep(0) # report a message print('Main done') # run the asyncio program asyncio.run(main()) |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task.
It then sleeps for zero seconds, suspending and allowing the task to run.
The task runs, reports a message, and starts to sleep.
The main() coroutine resumes, reports a message, and exits normally.
It closes the event loop and then the main thread of the program. The running independent task is terminated along with the event loop.
This highlights that a running task will not prevent the event loop from exiting.
1 2 |
Task starting Main done |
Does a Subprocess Stop the Event Loop Exiting
We can explore if the event loop will stop if a command is being executed in a subprocess.
A command is a program on the operating system that may be executed directly or via the shell. If executed by the shell, we can use shell scripting language elements and variables.
We can run a command from an asyncio program using the asyncio.create_subprocess_shell() function.
You can learn more about running commands from asyncio via the shell in the tutorial:
This will run the command and return a subprocess.
We can execute a command from the main coroutine, suspend to allow the command to execute, then return from the main coroutine. The command may continue to run and may or may not block the event loop, preventing it from terminating.
The example below explores this scenario.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# SuperFastPython.com # test if a subprocess stops the event loop from exiting import asyncio # task coroutine async def work(): # report a message print('Task starting') # run a subprocess process = await asyncio.create_subprocess_shell('sleep 2;echo hi') # wait for the subprocess to finish await process.wait() # report a message print('Task done') # main coroutine async def main(): # schedule the task task = asyncio.create_task(work()) # suspend a moment, allow the subprocess to start await asyncio.sleep(1) # report a message print('Main done') # run the asyncio program asyncio.run(main()) # report a message print('Event loop is closed') |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates the work() coroutine and schedules it as a task.
It then sleeps for one second, suspending and allowing the task to run.
The task runs, reports a message, then executes the command and waits for the command to complete.
The command begins executing, running the “sleep” command for two seconds.
The main() coroutine resumes and exits.
This terminates the event loop and the asyncio task.
The subprocess running the command continues to run. It finishes the sleep command and runs the echo command reporting a message on standard out.
This example highlights that exiting the asyncio event loop does not terminate running subprocesses.
1 2 3 4 |
Task starting Main done Event loop is closed hi |
Does a Thread Stop the Event Loop From Exiting
We can explore whether running a function in a new thread will stop the event loop from executing.
A blocking function can be run in a new thread from asyncio via the asyncio.to_thread() function. This returns a coroutine that can be awaited or scheduled as a task.
You can learn more about running blocking functions in a new thread in the tutorial:
We can execute a blocking function in a new thread and schedule it as a task, suspend to allow it to begin executing, then return from the main coroutine.
The blocking function may continue to run or not, and it may or may not prevent the asyncio event loop from terminating immediately.
The example below explores this scenario.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# SuperFastPython.com # test if a task stops the event loop from exiting import time import asyncio # task coroutine def work(): # report a message print('Task starting') # simulate work time.sleep(2) # report a message print('Task done') # main coroutine async def main(): # run function in a separate thread coro = asyncio.to_thread(work) # schedule the task task = asyncio.create_task(coro) # suspend a moment, allow the new thread to start await asyncio.sleep(1) # report a message print('Main done') # run the asyncio program asyncio.run(main()) # report a message print('Event loop is closed') |
Running the example creates the main() coroutine and runs it as the entry point for the asyncio program.
The main() coroutine runs and creates a coroutine to run the blocking function in a new thread.
It then schedules the coroutine as an independent task and sleeps for one second, suspending and allowing the task to execute.
The task begins running in a new thread, reporting a message, and sleeping.
The main() coroutine resumes, reports a message, and exits.
This does not terminate the event loop. The event loop blocks, waiting for the new thread to complete.
The blocking function in the new thread resumes and reports a final message before exiting.
The event loop terminates and a final message is reported that the event loop has been closed and the main thread is back in control.
This highlights that a blocking thread running in the asyncio event loop can prevent the event loop from exiting.
1 2 3 4 |
Task starting Main done Task done Event loop is closed |
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 how to exit the asyncio event loop in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Anurag singh on Unsplash
Do you have any questions?