You can use the asyncio server asynchronous context manager by the “async with” expression.
The context manager interface on the asyncio server will automatically close the server and all client connections. This is helpful to ensure that the server is closed safely, such as on error, when the server task is canceled, and when the program is terminated via a SIGINT signal.
In this tutorial, you will discover how to use the asynchronous context manager interface on the asyncio.Server.
Let’s get started.
Asyncio Server Context Manager
The asyncio.Server allows us to accept client socket connections in asyncio programs.
We can create a server via the asyncio.start_server() function.
For example:
1 2 3 |
... # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) |
You can learn more about creating an asyncio server in the tutorial:
The asyncio.Server class implements the asynchronous context manager interface used via the “async with” expression.
If you are new to asynchronous context managers, you can learn more in the tutorial:
The body of the context manager can be used to accept client connections. When the context manager is exited, the server is closed to new client connections, and all current client connections are closed.
For example:
1 2 3 4 |
... # open the server context manager async with server: # ... |
We can use the body of the context manager to accept client connections via the serve_forever() method.
For example:
1 2 3 4 5 |
... # open the server context manager async with server: # accept client connections forever await server.serve_forever() |
This is helpful for a few reasons, for example:
- If the asyncio task running the server is canceled, the connections will be closed safely.
- If the server fails with an unexpected exception, the connections will be closed safely.
- If the process running the server is terminated via a SIGINT (control-c), the connections will be closed safely.
Without the asynchronous context manager, it is possible for the server to be killed abruptly and client connections will not close safely.
This could be overcome manually using a try-finally block and manually closing the server and client connections.
For example:
1 2 3 4 5 6 7 8 9 |
... try: # accept client connections forever await server.serve_forever() finally: # close the server server.close() # wait for all client connections to close await server.wait_closed() |
This achieves the same effect as the asynchronous context manager, although it uses more code. The context manager provides a simpler and clearer solution.
Now that we know about the asyncio server context manager, let’s look at some worked examples.
Run loops using all CPUs, download your FREE book to learn how.
Example Asyncio Server Context Manager with Cancellation
We can explore an example of canceling the asyncio task running the server and confirming that the server was closed correctly via the context manager.
In this example, we will run the asyncio server in a background task, allow the server to run for a while, then cancel the task that is running the server. The expectation is that the context manager will safely close the server automatically. We will then check the status of the server after the context manager and confirm that it was closed.
Firstly, we can define the handler for client connections.
In this case, we don’t expect any client connections, so the handler will be empty.
1 2 3 |
# handler for client connections async def handler(reader, writer): pass |
Next, we can define the coroutine that will run the server as a background task.
Firstly, we will create a server configured to expect connections on port 8888. Once created, we will report the details of the server.
We will then open the context manager interface and accept client connections via the server_forever() method.
The context manager is then wrapped in a try-finally structure and the finally block reports the serving status of the server.
Tying this together, the background_server() coroutine below implements this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # ensure the server is closed correctly async with server: # accept client connections forever await server.serve_forever() finally: # confirm the server has stopped print(f'Server closed: serving={server.is_serving()}') |
Next, we can define the main coroutine.
Firstly, we will create and schedule a task for our background_server() coroutine.
We will then suspend for two seconds and allow the server to be created and start running.
The main() coroutine will then cancel the task running the server and wait for the task to be done.
This is called the cancel-and-wait idiom. You can learn more about it in the tutorial:
Tying this together, the main() coroutine is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass |
Finally, we can start the asyncio event loop.
1 2 3 |
... # start the asyncio event loop asyncio.run(main()) |
Tying this together, 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 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# SuperFastPython.com # example of asyncio server context manager and cancellation import asyncio # handler for client connections async def handler(reader, writer): pass # task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # ensure the server is closed correctly async with server: # accept client connections forever await server.serve_forever() finally: # confirm the server has stopped print(f'Server closed: serving={server.is_serving()}') # main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass # start the asyncio event loop asyncio.run(main()) |
Running the example first starts the event loop and runs the main() coroutine.
The main() coroutine runs and creates and schedules the background_server() as a background task. It then suspends with a sleep for two seconds.
The background_server() ask runs and creates the server and reports its details. It then enters the asynchronous context manager and suspends, accepting client connections forever.
The main() coroutine resumes and reports a message. It then cancels the background_server() task and waits for the task to be done.
A CancelledError exception is raised in background_server(). The context manager is exited, the server is closed and any client connections are closed (none in this case).
The finally block executes and reports the status of the server, confirming that the context manager closed it when it exited.
This highlights that the asyncio server asynchronous context manager will automatically close the server when the task running the server is canceled.
1 2 3 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> Shutting down... Server closed: serving=False |
Next, let’s explore an example of the context manager closing the server when the process is terminated with a SIGINT.
Example Asyncio Server Context Manager with SIGINT (KeyboardInterrupt)
We can explore the case of the asyncio server context manager safely closing the server when the process running the server is terminated with a SIGINT signal.
Recall that the SIGINT (signal interrupt) is a signal sent to a process when the user presses the Control-C key combination. It typically terminates the program that receives the signal.
In this case, we will update the example above to no longer cancel the task running the server. Instead, it will raise a SIGINT signal within the current Python process.
This can be achieved via the signal.raise_signal() function.
For example:
1 2 3 |
... # send signal to the current process signal.raise_signal(signal.SIGINT) |
The asyncio event loop is configured to handle the SIGINT and cancel the main task. When the main task exits, the event loop will cancel any remaining tasks.
When signal.SIGINT is raised by Ctrl-C, the custom signal handler cancels the main task by calling asyncio.Task.cancel() which raises asyncio.CancelledError inside the main task. This causes the Python stack to unwind, try/except and try/finally blocks can be used for resource cleanup. After the main task is cancelled, asyncio.Runner.run() raises KeyboardInterrupt.
— Runners, Asyncio API Documentation
It will then sleep for a moment to allow the signal to take effect and terminate the process.
1 2 3 |
... # wait for the signal await asyncio.sleep(2) |
The updated main() coroutine with this change is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 |
# main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # send signal to the current process signal.raise_signal(signal.SIGINT) # wait for the signal await asyncio.sleep(2) |
Tying this together, 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 29 30 31 32 33 34 35 36 37 38 39 |
# SuperFastPython.com # example of asyncio server context manager and SIGINT import asyncio import signal # handler for client connections async def handler(reader, writer): pass # task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # ensure the server is closed correctly async with server: # accept client connections forever await server.serve_forever() finally: # confirm the server has stopped print(f'Server closed: serving={server.is_serving()}') # main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(2) # report progress print('Shutting down...') # send signal to the current process signal.raise_signal(signal.SIGINT) # wait for the signal await asyncio.sleep(2) # start the asyncio event loop asyncio.run(main()) |
Running the example first starts the event loop and runs the main() coroutine.
The main() coroutine runs and creates and schedules the background_server() as a background task. It then suspends with a sleep for two seconds.
The background_server() ask runs and creates the server and reports its details. It then enters the asynchronous context manager and suspends, accepting client connections forever.
The main() coroutine resumes and reports a message. It then raises a SIGINT signal in the current Python process and then sleeps and awaits the signal to take effect.
The SIGINT is handled by the asyncio event loop, canceling the main task. The event loop exits and cancels the task running the server.
A CancelledError exception is raised in background_server(). The context manager is exited and the server is closed and any client connections are closed (none in this case). The finally block executes and reports the status of the server, confirming that the context manager closed it when it exited.
This highlights that the asyncio server asynchronous context manager will automatically close the server when the process running the server is terminated with a SIGINT.
1 2 3 4 5 6 7 8 9 10 11 12 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> Shutting down... Server closed: serving=False Traceback (most recent call last): … asyncio.exceptions.CancelledError During handling of the above exception, another exception occurred: Traceback (most recent call last): … KeyboardInterrupt |
Next, let’s confirm that clients are safely disconnected when the context manager is exited.
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.
Example Confirming Asyncio Server Context Manager Closes Server
We can explore whether connected clients are disconnected from the asyncio server safely when the server’s context manager is exited.
In this case, we will run the server for a moment, then develop and run a client that connects to the server. The asyncio task running the server will then be canceled and we observe the effect on both the server and the currently connected client.
Let’s dive in.
Asyncio Server
We will use the same server that we developed in the first example above.
In this case, we need to implement the callback used to handle the connected client. The callback will report a message and then sleep for 10 seconds.
1 2 3 4 5 6 |
# handler for client connections async def handler(reader, writer): # report progress print('Client connected') # wait a while await asyncio.sleep(10) |
We will also update it so that the server runs for 5 seconds before the main coroutine cancels the task running the server. This will give us enough time to start the server, then start the client before the server is closed.
1 2 3 |
... # wait around for a while await asyncio.sleep(5) |
The updated main() coroutine with this change is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(5) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass |
Tying this together, 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# SuperFastPython.com # example of asyncio server context manager and cancellation import asyncio # handler for client connections async def handler(reader, writer): # report progress print('Client connected') # wait a while await asyncio.sleep(10) # task for running the server in the background async def background_server(): # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # report the details of the server print(server) try: # ensure the server is closed correctly async with server: # accept client connections forever await server.serve_forever() finally: # confirm the server has stopped print(f'Server closed: serving={server.is_serving()}') # main coroutine async def main(): # start and run the server as a background task server_task = asyncio.create_task(background_server()) # wait around for a while await asyncio.sleep(5) # report progress print('Shutting down...') # cancel the server task server_task.cancel() # wait for the server to shutdown try: await server_task except asyncio.CancelledError: pass # start the asyncio event loop asyncio.run(main()) |
Next, let’s develop the client.
Asyncio Client
The client is very simple.
It connects to the server, reports a message that it is connected, and then sleeps for 10 seconds before closing the connection.
The complete client program is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# SuperFastPython.com # client for our test server import asyncio # test client async def main(): print('Connecting...') # open a connection to the server reader, writer = await asyncio.open_connection('127.0.0.1', 8888) # report message print('Connected') # wait around await asyncio.sleep(10) # close the connection print('Shutting down...') writer.close() await writer.wait_closed() # run the event loop asyncio.run(main()) |
Next, let’s run both client and server.
Example Exchange
We can now run the server, connect the client, and observe both as the server task is canceled and the server context manager closes the server safely.
We can save the server program to the file server.py.
The server can then be run from the command line using the Python command.
For example:
1 |
python server.py |
This starts the server, allowing clients to connect.
The client program can be saved to the file client.py and run from the command line in the same manner.
For example:
1 |
python client.py |
The client then connects to the server.
The client sleeps after it is connected. Similarly, the handler for the client in the server sleeps after the client is connected.
The main() coroutine resumes after 5 seconds and cancels the task running the server.
The context manager closes the server and the connection to the client. And the program exits.
A never-retrieved exception is then reported by the asyncio event loop after the program is exited.
A sample output from the server program is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)> Client connected Shutting down... Server closed: serving=False Exception ignored in: <function StreamWriter.__del__ at 0x107bda0c0> Traceback (most recent call last): File ".../lib/python3.11/asyncio/streams.py", line 395, in __del__ self.close() File ".../lib/python3.11/asyncio/streams.py", line 343, in close return self._transport.close() ^^^^^^^^^^^^^^^^^^^^^^^ File ".../asyncio/selector_events.py", line 860, in close self._loop.call_soon(self._call_connection_lost, None) File ".../asyncio/base_events.py", line 761, in call_soon self._check_closed() File ".../asyncio/base_events.py", line 519, in _check_closed raise RuntimeError('Event loop is closed') RuntimeError: Event loop is closed |
The client disconnects normally, as we might expect.
A sample output from the client program is listed below.
1 2 3 |
Connecting... Connected Shutting down... |
Next, let’s take a look at a common error when using the context manager for the asyncio server.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Common Error When Using Asyncio Server Context Manager
A common error when using the asynchronous context manager for the asyncio server is to create the server as part of the “async with” expression.
For example:
1 2 3 4 5 |
... # ensure the server is closed correctly async with asyncio.start_server(handler, '127.0.0.1', 8888): # accept client connections forever await server.serve_forever() |
This will result in a TypeError.
The reason is because the asyncio.start_server() returns a coroutine, not an object that implements the asynchronous context manager interface.
This breaks the expectation of the “async with” expression that expects to be provided an object that implements the __aenter__() and __aexit__() methods.
We can demonstrate this with a worked example, listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# SuperFastPython.com # example of attempting to create server in "async with" expression import asyncio # handler for client connections async def handler(reader, writer): pass # main coroutine async def main(): # ensure the server is closed correctly async with asyncio.start_server(handler, '127.0.0.1', 8888): # accept client connections forever await server.serve_forever() # start the asyncio event loop asyncio.run(main()) |
Running the example fails with exceptions, including a TypeError
1 2 3 |
RuntimeWarning: coroutine 'start_server' was never awaited RuntimeWarning: Enable tracemalloc to get the object allocation traceback TypeError: 'coroutine' object does not support the asynchronous context manager protocol |
The correct usage of the context manager is to first call and await the asyncio.start_server() coroutine to create an asyncio.Server instance.
The asyncio.Server instance implements the asynchronous context manager interface, which can then be used within the “async with” expression.
For example:
1 2 3 4 5 6 7 |
... # create an asyncio server server = await asyncio.start_server(handler, '127.0.0.1', 8888) # ensure the server is closed correctly async with server: # accept client connections forever await server.serve_forever() |
One way to combine the creation of the server and the “async with” expression is to use the := operator (also called the walrus operator).
For example:
1 2 3 4 5 |
... # ensure the server is closed correctly async with (server:= await asyncio.start_server(handler, '127.0.0.1', 8888)): # accept client connections forever await server.serve_forever() |
This awaits the creation of the server, assigns it to the “server” variable and returns the asyncio server object. This happens within a sub-expression because of the brackets. The “async with” expression then uses the returned asyncio server object from the expression.
The complete example below demonstrates this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# SuperFastPython.com # example of correctly creating the server in the "async with" expression import asyncio # handler for client connections async def handler(reader, writer): pass # main coroutine async def main(): # ensure the server is closed correctly async with (server:= await asyncio.start_server(handler, '127.0.0.1', 8888)): # accept client connections forever await server.serve_forever() # start the asyncio event loop asyncio.run(main()) |
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 use the asyncio server context manager interface in Python programs.
Did I make a mistake? See a typo?
I’m a simple humble human. Correct me, please!
Do you have any additional tips?
I’d love to hear about them!
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Jakob Rosen on Unsplash
Do you have any questions?