Z. Qui
Z. Qui

Reputation: 199

python asynchronous context manager

In Python Lan Ref. 3.4.4, it is said that __aenter__() and __aexit__() must return awaitables. However, in the example async context manager, these two methods return None:

class AsyncContextManager:
    async def __aenter__(self):
        await log('entering context')

    async def __aexit__(self, exc_type, exc, tb):
        await log('exiting context')

Is this code correct?

Upvotes: 4

Views: 9463

Answers (2)

user2357112
user2357112

Reputation: 281758

__aenter__ and __aexit__ must return awaitables, but look what happens when you call the ones in the example:

>>> class AsyncContextManager:
...     async def __aenter__(self):
...         await log('entering context')
...     async def __aexit__(self, exc_type, exc, tb):
...         await log('exiting context')
... 
>>> AsyncContextManager().__aenter__()
<coroutine object AsyncContextManager.__aenter__ at 0x7f5b092d5ac0>

It didn't return None! We got a coroutine object, which is awaitable.

These methods are async functions, which automatically return (awaitable) asynchronous coroutines. return statements in the body of an async function determine what gets returned when you await the coroutine, not what gets returned when you call the function.

This is similar to how generator functions return generator iterators, even though they usually have no return statement, and how if you write __iter__ as a generator function, you should not try to return an iterator inside the generator function.


So what happens if you do put a return in __aenter__ or __aexit__ defined as async functions? Well, you can, and if you do, the return statement doesn't have to return an awaitable. Here's what Python will do.

If you return something from an __aenter__ defined as an async function, that determines what gets bound to an as target, if the async with uses as.

If you return something from an __aexit__ defined as an async function, that determines whether to suppress exceptions that got raised inside the block. A "truthy" value tells the async with to suppress exceptions, while a "falsy" value tells the async with to let exceptions propagate. The default None is falsy, so by default, exceptions are not suppressed.

Here's an example:

import asyncio

class Example:
    async def __aenter__(self):
        return 3
    async def __aexit__(self, exc_type, exc, tb):
        return True

async def example():
    async with Example() as three:
        print(three == 3)
        raise Exception
    print("Exception got suppressed")

asyncio.run(example())

Output:

True
Exception got suppressed

Upvotes: 7

Tim tj timer Jedro
Tim tj timer Jedro

Reputation: 47

Your __aenter__ method must return a context.

class MyAsyncContextManager:
    async def __aenter__(self):
        await log('entering context')
        # maybe some setup (e.g. await self.setup())
        return self

    async def __aexit__(self, exc_type, exc, tb):
        # maybe closing context (e.g. await self.close())
        await log('exiting context')

    async def do_something(self):
        await log('doing something')

usage:

async with MyAsyncContextManager() as context:
    await context.do_something()

Upvotes: 3

Related Questions