DEV Community

Main
Main

Posted on • Originally published at pynerds.com

async/await in Python

The async and await keywords are used to manage asynchronous tasks. 

Asynchronous programming allows multiple tasks to be processed simultaneously without blocking other tasks. In Python, it is achieved through the use of coroutines.

The async keyword is used to define a function as an asynchronous coroutine, and the await keyword is used inside of a coroutine function to wait for the execution of another coroutine.

What are coroutines?

Coroutines are just like regular functions with the additional capability of  pausing and resuming their execution at specific points. This allows for non-blocking behavior, where the execution of a coroutine can be suspended in case of delays or to deliberately allow other code to run before resuming its execution.

Coroutine are very closely related to generator functions. In fact, prior to the introduction of async and await in Python 3.5, generator functions were used in a special way to implement coroutines.

With python 3.5 and later versions, we can now create coroutines more conveniently and natively using the two keywords, async and await.

  1. async creates a coroutine function.
  2. await suspends a coroutine to  allow another coroutine to be executed. 

Define coroutine functions with async

Defining coroutine functions is much like defining regular functions. Consider the following example:

Define  a regular function

def add(a, b):
    print(f'{a} + {b} = {a + b}')

With a regular function, we use the def keyword in the declaration, with a coroutine function, we use async def instead of just def.

Define a coroutine function

async def add(a, b):
    print(f'{a} + {b} = {a + b}')

Calling a coroutine function

To call a regular function, you simply use its name followed by parenthesis with the necessary arguments. 

Call a regular function

def add(a, b):
    print(f'{a} + {b} = {a + b}')

add(10, 20)

Unlike with regular functions, calling a coroutine function does not immediately execute the code inside the function. Instead, it creates a coroutine object.

async def add(a, b):
    print(f'{a} + {b} = {a + b}')

#call a coroutine
coro = add(10, 20) #creates a coroutine object
print(coro)

To actually run the code inside the coroutine function, you need to await it in what is referred to as an event loop. The most direct way to do this is by using the run() function in the asyncio module of the standard library.

import asyncio

async def add(a, b):
    print(f'{a} + {b} = {a + b}')

#create a coroutine object
coro = add(10, 20) 

#run the coroutine
asyncio.run(coro) #executes the code in the coroutine

The asyncio.run() function creates an event loop,  runs the specified coroutine and returns the result of the coroutine. It is designed to be used for running the main function of an asynchronous application or to run a simple one-off coroutine.

Without asyncio.run() you would be needed to manually, initialize and manage the event loop, create and schedule tasks, and wrap the code in a coroutine. This would be more complicated and error-prone compared to using asyncio.run(), which handles all of these steps automatically.

more examples

import asyncio

async def add(a, b):
    print(f'{a} + {b} = {a + b}')

asyncio.run(add(50, 30))
asyncio.run(add(50, 50))
asyncio.run(add(90, 10))

Suspending coroutines with await

The await keyword is used to pause the execution of a coroutine until another inner coroutine is finished. Once the awaited coroutine has finished, the original coroutine will resume its execution from where it left off.

await <coro()>

The await keyword takes a single argument, coro(), a coroutine object to be awaited.

Consider the following example.

await a coroutine 

import asyncio

#prints even numbers from 0-n
async def display_evens(n):
    for i in range(n):
        if i % 2 == 0:
            print(i)

async def main():
      print("Started!")
      await display_evens(10) #await display_evens()
      print('Finished!')

#run main
asyncio.run(main())

In the above example, when the await statement is encountered, the execution of main is suspended until the display_evens() coroutine is fully executed. Multiple coroutines can be awaited as necessary.

await multiple coroutines

import asyncio

#prints even numbers from 0-n
async def display_evens(n):
    for i in range(n):
        if i % 2 == 0:
            print(i)
        await asyncio.sleep(0.01)#cause a small delay 

#prints even numbers from 0-n
async def display_odds(n):
    for i in range(n):
        if i % 2 == 1:
            print(i)
        await asyncio.sleep(0.01)#cause a small delay

async def main():
      print("Started!")
      await display_evens(10)
      await display_odds(10) 
      print('Finished!')

#run main
asyncio.run(main())

In the above example, the awaited coroutines, display_evens() and display_odds() are executed one after the other. In the following section, we will see how to make awaited coroutines to be executed simultaneously.

Executing awaited coroutines in parallel.

The primary goal of asynchronous programming is to execute multiple tasks in parallel. This means that while one task is executing, another task can start and run simultaneously.

To run multiple coroutines in parallel and in a non-blocking manner, we can combine them into a single awaitable object using the asyncio.gather() function, then await the returned object.

execute multiple coroutines

import asyncio

#prints even numbers from 0-n
async def display_evens(n):
    for i in range(n):
        if i % 2 == 0:
            print(i)
        await asyncio.sleep(0.01)#cause a small delay 

#prints even numbers from 0-n
async def display_odds(n):
    for i in range(n):
        if i % 2 == 1:
            print(i)
        await asyncio.sleep(0.01)#cause a small delay

async def main():
      print("Started!")
      await asyncio.gather(display_evens(10), display_odds(10)) #gathered coroutines will run in parallel
      print('Finished!')

#run main
asyncio.run(main())

If you observe the above output, you will see that both display_evens() and display_odds() are executed simultaneously. The outputs from the two functions are emitted alternately making it look as if it was just a single function printing all the integers.

Conclusion

  • Asynchronous programming makes it possible to execute tasks in parallel.
  • A coroutine is just like a regular function except that it is capable of pausing its execution and resuming later on at the same point.
  • The async keyword creates coroutine objects.
  • The await keyword suspends the execution of a coroutine until an inner coroutine is executed fully.
  • The asyncio.run() function eases event loop management. It executes a single coroutine.
  • The asyncio.gather() function, combines multiple coroutines into a single awaitable object. 

Related articles

Top comments (0)