The with statement is used with asynchronous context managers. But let us first understand how context managers works in general.
Context managers provides convenience when working with objects that require set up and tear down operations. Typically, these are the objects that can be used in a with statement such as files, streams, sockets, database, etc.
Traditionally, a context manager object must implement the context manager protocol which consists of two methods i.e __enter__()
and __exit__()
.The __enter__()
method performs the setup operation while the __exit__()
method performs the tear down operation. For example in case with files, the __enter__()
method will open the file either for read, write or both, while the __exit__()
method closes the file and releases any associated resources.
Any object that implements the context manager protocol is eligible for use in a with
statement.
When using the with
statement, the two methods of a context manager object i.e __enter__()
and __exit__()
are called automatically. __enter__()
is called automatically as the first method before entering the with
block while the __exit__()
method is called immediately before leaving the block.
To understand this better let us implement a very simple context manager object.
import asyncio
class Example:
def __enter__(self, *args, **kwargs):
print("__enter__() called")
def __exit__(self, *args, **kwargs):
print("__exit__() called")
with Example():
pass
As you can see above the two method have been called automatically, when we used the object in a with
statement.
The with
statement ensures that the __exit__()
method of an object is called no matter how the block exists, even if due to an exception being raised.
In our example, the __enter__()
method doesn't return anything, it simply displays a massage showing that it was called, but in most practical cases, it usually returns an instance of the context manager object through self
.
import asyncio
class Example:
def __enter__(self, *args, **kwargs):
print("__enter__() called")
return self
def __exit__(self, *args, **kwargs):
print("__exit__() called")
with Example() as manager:
print(manager)
As shown above, the object returned by __enter__()
is accessible within the with
statement through the alias name used in the as
clause.
Asynchronous context managers
We have seen how regular context managers work, now let us look at asynchronous context managers.
Asynchronous context managers must also implement two methods i.e __aenter__()
and __aexit__()
.
Unlike the regular context manager, an asynchronous context manager is capable of pausing the execution of its enter and exit methods. This is very useful as connection to some context manager objects like sockets and databases may take time or face delays. In case of delays, the asynchronous context manager can pause the current task, move to another task and then return later on.
When working with asynchronous context managers, we need to use the async with
statement rather than just with
as in traditional context managers.
a basic async context manager
import asyncio
class AsyncExample:
def do_something(self):
print('Hello, World!')
async def __aenter__(self, *args, **kwargs):
print("__aenter__() called")
await asyncio.sleep(0.05) #simulate a delay
return self
async def __aexit__(self, *args, **kwargs):
print("__aexit__() called")
await asyncio.sleep(0.005) #simulate a delay
async def main():
print('Started ')
async with AsyncExample() as manager:
#within the manager
manager.do_something()
print("Finished")
asyncio.run(main())
It is very important to note that the context manager methods i.e __aenter__()
and __aexit__()
are implemented as coroutines i.e with async def
.
The __aenter__()
method displays a message showing that it was called and then returns an instance of the manager(through self
) which then becomes accessible inside the with block through the alias name used in the as
clause i.e manager in the above case.
The async with
statement cannot be used on its own, it has to be inside of a coroutine(an async function). Trying this will raise a SyntaxError
exception as demonstrated below.
import asyncio
class AsyncExample:
async def __aenter__(self, *args, **kwargs):
print("__aenter__() called")
async def __aexit__(self, *args, **kwargs):
print("__aexit__() called")
async with AsyncExample() as manager:
pass
asyncio.run(main())
Asynchronous context managers without "async with"
While it is generally considered a good practice to use asynchronous context managers in an "async with
" statement and traditional context managers in a regular with
statement, it is also possible to use them manually outside of the with statements. This involves explicitly calling the set up and tear down methods.
For example, you can open a file in two ways. One is within a with
statement as shown below:
with open('myfile.txt') as f:
#do something
In the above case, the file will be closed automatically. The other approach where we do not use the with
statement requires you to manually perform the tear down operation by calling the close()
method.
f = open('myfile.txt') #setup operation
f.read() #do something
f.close() #teardown operation.
The same can be replicated with an asynchronous object so that it has two interfaces, one which will be used automatically by the async wit
h statement and the second which we can use manually.
Conclusion
- Context managers are normally used with objects that require set up annd tear down operations.
- A context manager object must implement the enter and exit methods.
- Traditional context manager implements
___enter__()
and__exit__()
methods. - Asynchronous context manager implements
__aenter__()
and__aexit__()
methods. - Asynchronous context manager can pause the execution of either
__aenter__()
or__aexit__()
in case of delays or other factors. - The
with
statement is used to manage traditional context managers. - The
async with
statement is used to manage asynchronous context managers.