Description
Antipattern: The following code is unsafe:
async def yield_some_values():
async with some_async_context_manager():
for _ in range(5):
...
yield some_value # error: `yield` inside `async with` block
async def bad_method():
async for value in yield_some_values():
raise Exception("exiting early")
Explanation: If the iterator is not fully consumed, the cleanup path will only be triggered by the garbage collector, which will not allow the async cleanup logic to await
(it will raise GeneratorExit
when it tries). A similar issue arises if you try to await
in a finally
block in an async generator, or an except
block that can intercept asyncio.exceptions.CancelledError
.
The only exception to this that I'm aware of is if the async generator is decorated with @contextlib.asynccontextmanager
.
Fix: I think unfortunately the only fix is to refactor the API to separate the context management from the iteration, e.g.
@contextlib.asynccontextmanager
async def yield_some_values_safely():
async with some_async_context_manager():
async def inner_iterator():
for _ in range(5):
...
yield some_value
yield inner_iterator # safe because this is in an asynccontextmanager
async def bad_method():
async with yield_some_values_safely() as values:
async for value in values:
raise Exception("exiting early")
I'm not aware of any existing lint check for this antipattern (I may have missed it though!) — do the maintainers of this repo think it would be valuable?