Python asyncio.Runner() context manager (with examples)

Updated: July 11, 2023 By: Khue Post a comment

The Fundamentals

An asyncio.Runner() context manager is a new feature in Python 3.11 that simplifies running multiple async functions in the same context. It takes care of creating and closing an asyncio event loop, as well as managing the contextvars.Context for the async functions.

To use it, you need to create an instance of asyncio.Runner() with the with statement, and then call its run() method with the async function you want to execute. You can also pass a custom context to the run() method if you want to use a different contextvars.Context for the async function. The runner will return the result or raise the exception of the async function.

Here is an example of using asyncio.Runner() to run two async functions that print some messages with delays:

import asyncio

async def first():
  await asyncio.sleep(1)
  print("Welcome to Sling Academy!")

async def second():
  await asyncio.sleep(2)
  print("Have a nice day and have fun with Python!")

with asyncio.Runner() as runner:
  runner.run(first())
  runner.run(second())

This will output two messages (the second one will appear two seconds after the first one):

Welcome to Sling Academy!
Have a nice day and have fun with Python!

The close() method of asyncio.Runner() is used to close the runner and release its resources. It will finalize any asynchronous generators, shut down the default executor, close the event loop, and release the embedded contextvars.Context. You should call this method when you are done using the runner or use the with statement (like the example above) to automatically close it at the end of the block.

Advanced example

The example in the preceding section is basic, and you can get bored with it. Let’s examine a little bit more advanced program.

This example creates two coroutines that perform different tasks: one prints the current time every second for 10 seconds, and the other prints a message after 5 seconds. It then uses an asyncio runner to run both coroutines in the same event loop, one after the other. This demonstrates how to use the asyncio runner to execute multiple coroutines without creating a wrapper coroutine or multiple event loops.

import asyncio
import time

# A coroutine that prints the current time
async def print_time():
    print(time.strftime("%X"))

# A coroutine that waits for a given number of seconds
async def wait(seconds):
    await asyncio.sleep(seconds)

# A coroutine that prints the current time every second for 10 seconds
async def print_time_every_second():
    for _ in range(10):
        await print_time()
        await wait(1)

# A coroutine that prints a message after 5 seconds
async def print_message():
    await wait(5)
    print("Welcome to Sling Academy!")

# Create an asyncio runner
with asyncio.Runner() as runner:
    # Run the first coroutine
    runner.run(print_time_every_second())
    # Run the second coroutine
    runner.run(print_message())

Output (it depends on when you run the code):

22:34:23
22:34:24
22:34:25
22:34:26
22:34:27
22:34:28
22:34:29
22:34:30
22:34:31
22:34:32
Welcome to Sling Academy!

This example uses the context manager interface of the asyncio.Runner() class, which will close the event loop for us when we’re finished with it. It also uses two coroutines: print_time_every_second() and print_message(), which are executed in the same event loop by calling the run() method of the runner.

The difference between asyncio.Runner() and asyncio.run()

The asyncio.Runner() context manager is different from asyncio.run() function in several ways. The asyncio.run() function always creates a new event loop and closes it at the end, and it can only run one async function at a time. It is meant to be used as a main entry point for asyncio programs and, ideally, only be called once.

The asyncio.Runner() context manager allows you to reuse the same event loop and context for multiple async functions, and it also lets you pass a custom context to each async function if you want. It is meant to be used for scenarios where you need to run several top-level async functions in the same context, and it can be nested inside other async functions or contexts.