Asyncio Server Context Manager
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:
...
# 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:
...
# 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:
...
# 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:
...
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.
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.
# 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.
# 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.
# 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.
...
# start the asyncio event loop
asyncio.run(main())
Tying this together, the complete example is listed below.
# 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.
<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:
...
# 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.
...
# wait for the signal
await asyncio.sleep(2)
The updated main() coroutine with this change is listed below.
# 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.
# 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.
<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.
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.
# 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.
...
# wait around for a while
await asyncio.sleep(5)
The updated main() coroutine with this change is listed below.
# 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.
# 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.
# 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:
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:
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.
<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.
Connecting...
Connected
Shutting down...
Next, let's take a look at a common error when using the context manager for the asyncio server.
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:
...
# 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.
# 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
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:
...
# 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:
...
# 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.
# 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())
Takeaways
You now know how to use the asyncio server context manager interface in Python programs.
If you enjoyed this tutorial, you will love my book: Python Asyncio Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.