Python Friday #215: Async & Await

While our application waits on IO (Input / Output), our CPUs idle around. If we would have a way to let the program step aside and let other code run while we wait on a file or a response, we could get more work done at the same time. That is the basic idea behind asynchronous programming that we can do with coroutines and the async and await keywords in Python.

This post is part of my journey to learn Python. You find the code for this post in my PythonFriday repository on GitHub.

 

Async and await

The asynchronous code in Python depends on two parts: async and await. The async keyword goes in front of the function definition, while the await keyword goes in front of the slow IO part. Only when we have both things in place can we profit from the asynchronous behaviour.

To run our hi() method without a warning or an error message, we need to pass it to asyncio.run():

This will run our hi() function and it will first print “Hello”, then wait a second before it will print “World”. If there is no other code that should run at the same time, our code behaves as usual.

 

Why not time.sleep()?

An easy way to simulate work in our demo code is to use time.sleep() as a stand-in. That gives us the expected slowdown while we do not need to focus much on the implementation of the slow code.

Unfortunately, time.sleep() does not give us a behaviour that we expect with asynchronous code. While we can use it in a method with an async keyword, we cannot await it. And without the await, our code cannot step aside.

That is why we use asyncio.sleep(1) instead. We can use it with await and our code lets other code pass until the sleep time is over.

 

Run in parallel

The great benefit of async and await happens when we have multiple asynchronous methods that we can run together. For that we can create a function that calls asyncio.gather() with multiple hi() methods:

If we run this code, we can check the output to see that they run up to the await part, then step aside, and let another method run while they sleep for a second:

Hello
Hello
Hello
World
World
World

The whole run for the parallel() method took a little bit over a second, even when we had 3 runs of hi() that sleep for a second.

 

Running in sequence

We could also decide to run our asynchronous methods one after the other and write the runner for those calls to hi() like this:

In this case we cannot profit from asynchronous processing, and it takes around 3 seconds to get this output:

Hello
World
Hello
World
Hello
World

 

Do not forget the await part

Asynchronous programming can only help us if we mark the long running parts of our application with await. If we do not do that, our function runs synchronously and does not make space for other tasks, no matter how often we write async in front of our functions.

We can verify that by replacing the await asyncio.sleep(1) with time.sleep(1) in the hi() method:

If we run the parallel() method again, it will no longer benefit from asynchronous programming:

Hello
World
Hello
World
Hello
World

Why do we not just remove the await in front of the asyncio.sleep(1)? If we do that our code runs straight through the method and does not wait at all.

I hope this little detour helps you to understand that async and await are not magic wands that allow us to do everything faster. We need to use it in the right situation, and we need to follow through with all parts to get the expected benefits.

 

Next

This little excursion showed us how we can use async and await to tell Python what parts take longer and can step aside for other tasks. That works especially well with blocking resources like IO operations or our sleep() command.

Next week we continue with FastAPI and explore our options to test our API.

1 thought on “Python Friday #215: Async & Await”

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.