Async with Expression in Python (77/100 Days of Python)

Martin Mirakyan
3 min readMar 19, 2023
Day 77 of the “100 Days of Python” blog post series covering async with expression

Asynchronous programming has become an essential part of modern software development. It allows developers to write code that can perform multiple tasks simultaneously, thus improving performance and responsiveness. One of the most useful features of asynchronous programming in Python is the “async with” statement.

What is async with Statement in Python?

Async with is a statement that allows developers to use a context manager asynchronously. In Python, a context manager is an object that defines the methods __enter__() and __exit__(). The with statement is used to manage resources such as files, network connections, and locks. async with statement works in the same way but allows context managers to be used in asynchronous code.

async with works by defining an asynchronous context manager. An asynchronous context manager is a class that defines the methods aenter() and aexit(). When the async with statement is used, the aenter() method is called to obtain the context, and the aexit() method is called to release the context.

How to Define a Custom async with Context Manager?

Using async with is straightforward. You can use it just like you use the with statement. The only difference is that the context manager should be defined as an asynchronous context manager:

import asyncio


class AsyncContextManager:
async def __aenter__(self):
print('Entering the async context')
return self

async def __aexit__(self, exc_type, exc, tb):
print('Exiting the async context')


async def main():
async with AsyncContextManager():
print('Inside the async context')


asyncio.run(main())

This code will output:

Entering the async context
Inside the async context
Exiting the async context

As you can see, the code enters the async context, executes the code inside the async with statement, and then exits the context.

When to Use async with?

async with is useful when you want to manage resources asynchronously. For example, you can use async with to manage network connections, file I/O, and locks. Using async with can help you avoid blocking the event loop, which can lead to performance issues.

Using async with for Network Connections

When you are making network requests, it is important to manage connections efficiently. Using async with can help you manage network connections asynchronously. For example, the aiohttp library uses async with to manage HTTP sessions.

import asyncio

import aiohttp


async def main():
async with aiohttp.ClientSession() as session:
async with session.get('https://google.com') as response:
print(await response.text())


asyncio.run(main())

This program will get the contents of the google.com homepage and print the HTML.

File I/O

When you are reading or writing files, using async with can help you manage file I/O operations asynchronously. For example, the aiofiles library uses async with to manage file I/O.

import asyncio

import aiofiles


async def main():
async with aiofiles.open('file.txt', 'w') as file:
await file.write('Hello, World!')


asyncio.run(main())

This program is almost equivalent to opening a file and writing a Hello, World! message to it. Yet, in the async context, this can have the benefit of not blocking the main event loop.

Locks

When you need to synchronize access to a shared resource, using locks can help you avoid race conditions. Using async with can help you manage locks asynchronously. For example, the asyncio library uses async with to manage locks:

import asyncio

lock = asyncio.Lock()


async def main():
async with lock:
# Do something here...


asyncio.run(main())

What’s next?

--

--

Martin Mirakyan

Software Engineer | Machine Learning | Founder of Profound Academy (https://profound.academy)