How to use asyncio in Python

John MainJohn Main
7 min read

Original Article

asyncio is a module that provides tools for implementing asynchronous programming. It was introduced in Python 3.3 and has since become a popular choice for creating and managing asynchronous tasks.

The module is available in the standard library, and can therefore be imported and used directly without extra installations.
import the asyncio module

Example
import asyncio

print(asyncio)
Output:

<module 'asyncio' from '/app/.heroku/python/lib/python3.11/asyncio/__init__.py'>

Basic usage

Asynchronous programming makes it possible to execute multiple pieces of code in a non-blocking manner. The basic idea is to execute a task and when a delay is encountered, execution can switch to another task instead of blocking and waiting for the delay to finish.

Asynchronous tasks are created and managed using the async and await keywords in addition to the various functions defined in the asyncio module. Consider the following example:

Example

basic asyncio usage

#import the module
import asyncio

#define asynchronous tasks
async def task1():
    #prints even numbers from 0 to 9
    for i in range(10):
       if i%2 ==0:
           print(i)
           await asyncio.sleep(0.0001)#cause a small delay

async def task2():
     #prints odd numbers from 0 to 9
     for i in range(10):
         if i%2 == 1:
             print(i)
             await asyncio.sleep(0.0001) # cause a small delay

async def main():
    print('Started:')
    await asyncio.gather(task1(), task2())
    print('Finished!')

asyncio.run(main())
Output:

Started: 0 1 2 3 4 5 6 7 8 9 Finished!

In the above snippet, we:

  1. imported the asyncio module.

  2. Defined two asynchronous tasks(corourines), task1, and task2. task1 prints even numbers from 0 to 9 while task2 prints odd numbers in the same range. Inside each coroutine the asyncio.sleep() call simulates a delay in the execution of the coroutine, this makes the execution to move to the other coroutine.

  3. We defined the main coroutine, then awaited task1 and task2 inside it. The asyncio.gather() function combines multiple coroutines into a single awaitable object.

  4. We executed the main() coroutine using the asyncio.run() function.

  5. The result is that task1 and task2 are executed in parallel, as you can tell from the output.

The above example may look somehow complicated but this is the most complete basic example of a practical asynchronous program I can think of.

In the following sections, we will explore the two keywords and the various asyncio functions and what each does.

async/await keywords

Python provides the async and await keywords for creating coroutines. Coroutines are functions that can be executed asynchronously. To be more specific, coroutines are functions in which execution can be suspended and then resumed later on.

  1. async defines a function as an asynchronous coroutine.

  2. await suspends the running coroutine until another coroutine is fully executed.

The async keyword

Defining coroutine function is much like defining regular functions. We simply put the async keyword before def to tell the interpreter that this is a coroutine function

Example

Define a coroutine function

async def greet(name):
     print('Hello ' + name)
Output:

Unlike in a regular function, the statement in a coroutine do not get executed immediately when the coroutine is called. Instead, a coroutine object is created.

Example

calling a coroutine

async def greet(name):
     print('Hello ' + name)

coro = greet('Jane')#This does not execute the  statements in the coroutine
print(coro)#a coroutine object is created
Output:

<coroutine object greet at 0x7fa671749f20>

To actually execute the statements in a given coroutine, you have to await it in what is referred to as an event loop. Python provides a simpler way to do this through the asyncio.run() function.

The asyncio.run() function takes a single coroutine as the argument and then automatically performs tasks that a programmer would be required to handle manually, such as scheduling, creating an event loop, running the event loop, etc

Example
import asyncio

async def greet(name):
     print('Hello ' + name)

coro = greet('Jane')#This does not execute the  statements in the coroutine
asyncio.run(coro)#run the coroutine
Output:

Hello Jane

The await keyword

Consider what you would do if you wanted to run a coroutine inside of another coroutine. One might be tempted to use the asyncio.run() function inside the coroutine, but this would automatically lead to a RunTimeError exception.

Example
import asyncio

async def greet(name):
     print('Hello ' + name)

async def main():
      asyncio.run(greet('John'))
coro = main()
asyncio.run(coro)#runtime error
Output:

RuntimeError: asyncio.run() cannot be called from a running event loop

To run a coroutine inside of another coroutine, you need to use await keyword. The keyword has the following syntax:

Syntax:
await <coroutine>

await makes the running coroutine to be suspended until the awaited <coroutine> is completely executed.

Example
import asyncio

async def greet(name):
     print('Hello ' + name)

async def main():
      await greet('John')

coro = main()
asyncio.run(coro)
Output:

Hello John

Running coroutines simultaneously

The primary goal of asynchronous programming is to execute multiple tasks in a non-blocking manner. This means that a task does not have to wait until another task is fully executed in order to start.

To run multiple tasks in parallel you would be required to manually interact with the event loop as we will see later. But again, Python offers a simpler way to do this. The process is as illustrated below:

  1. Create asynchronous coroutines.

  2. Combine and schedule them using asyncio.gather() function

  3. Await the coroutine object returned by asyncio.gather() in another coroutine eg. main().

  4. Run main using asyncio.run() function.

Let us look again at the example that we started with.

Example
#import the module
import asyncio

#define asynchronous tasks
async def task1():
    #prints even numbers from 0 to 9
    for i in range(10):
       if i%2 ==0:
           print(i)
           await asyncio.sleep(0.0001)#cause a small delay

async def task2():
     #prints odd numbers from 0 to 9
     for i in range(10):
         if i%2 == 1:
             print(i)
             await asyncio.sleep(0.0001) # cause a small delay

async def main():
    print('Started:')
    await asyncio.gather(task1(), task2())
    print('Finished!')

asyncio.run(main())
Output:

Started: 0 1 2 3 4 5 6 7 8 9 Finished!

If you observe the above output, you will see that the two function are actually being executed at the same time as the output is emitted alternately. Or are they?

One thing that you should keep in mind is that true simultaneity is not achievable with asynchronous programming. In reality, the two tasks are not being executed simultaneously. The small delay caused by asyncio.sleep(0.0001) is enough to make the execution to switch between the two tasks.

Example
#import the module
import asyncio

#define asynchronous tasks
async def print_nums():
    for n in range(5):
       print(n)
       await asyncio.sleep(0.0001)#cause a small delay

async def print_letters():
     #prints odd numbers from 0 to 9
     for l in 'abcde':
        print(l)
        await asyncio.sleep(0.0001) # cause a small delay
async def main():
    print('Started:')
    await asyncio.gather(print_nums(), print_letters())
    print('Finished!')

asyncio.run(main())
Output:

Started: 0 a 1 b 2 c 3 d 4 e Finished!

This is essentially how asynchronous programming works, when a delay is encountered in a running coroutine, the execution is passed to the next coroutine in the schedule. This can be especially useful when dealing with tasks that may lead to blockage such as I/O bound tasks. As it allows the program to continue executing other tasks instead of being blocked until the task is completed.

The event loop

We have been talking about the event loop without formally introducing it.

In this section we will manually interact with event loops to run manage asynchronous tasks. We will see what the asyncio.run() function is doing in the background.

Creating an event loop

The new_event_loop() function in the asyncio module creates and returns an event loop.

Example

create a loop

import asyncio

loop = asyncio.new_event_loop()

print(loop)
print(loop.is_running())
print(loop.is_closed())

loop.close()
print(loop.is_closed())
Output:

<_UnixSelectorEventLoop running=False closed=False debug=False> False False True

In the above example, we created an event loop then used some methods to check the state of the loop. The is_running() method checks whether a loop is currently running. The is_closed() method checks whether a loop is closed.

You should always remember to call the close() method after you are done using the loop.

run a single coroutine in an event loop

The loop.run_until_complete() can be used to manually execute a one-off coroutine in an event loop.

Example
import asyncio

#the coroutine
async def task():
    print('Hello, World!')

loop = asyncio.new_event_loop() #create the loop
loop.run_until_complete(task()) #run the coroutine

loop.close()#close the loop
Output:

Hello, World!

Run multiple coroutines in an event loop

When multiple coroutines have to be executed simultaneously, we will be required to create an event loop, schedule the tasks in the event loop, the execute them. The following snippet shows a basic example:

Example
import asyncio

#the tasks
async def print_nums(num):
     for n in range(num):
          print(n)    
          await asyncio.sleep(0.0001)#simulate

async def print_letters(up_to):
     for letter in range(97,ord(up_to)):
          print(chr(letter))
          await asyncio.sleep(0.0001)#simulate delay

#create an event loop
loop = asyncio.new_event_loop()

#schedule the tasks for execution
loop.create_task(print_nums(5))
loop.create_task(print_letters('f'))

#get all the tasks scheduled in the loop
tasks = asyncio.all_tasks(loop = loop)
#create an awaitable for the tasks
group = asyncio.gather(*tasks)

#run the loop
loop.run_until_complete(group)

#close the loop
loop.close()
Output:

0 a 1 b 2 c 3 d 4 e

‹‹ Prev threading→


0
Subscribe to my newsletter

Read articles from John Main directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

John Main
John Main