Async Generators and Context Managers
The `async-generator` library provides a backport of asynchronous generators and asynchronous context managers to Python 3.5, which were introduced natively in Python 3.6 (PEP 525) and 3.7. It allows developers to write cleaner asynchronous code for stream processing and I/O-bound tasks using familiar generator syntax. Maintained by the Trio project, it is compatible with any async framework like asyncio or Trio. The current version is 1.10, and it is considered stable, with the last release in July 2018.
Warnings
- gotcha Async generators (especially when partially consumed or exited via `break`) do not guarantee `finally` block execution or proper resource cleanup without explicit closing. The garbage collector cannot await cleanup tasks.
- breaking For Python 3.5, you *must* use `await yield_(value)` within an `@async_generator` decorated function. Direct `yield value` (native async generator syntax) is a syntax error in Python 3.5 and will not work.
- gotcha A native async generator in Python 3.6+ that *only* contains `await yield_from_(...)` calls might not be correctly recognized as an async generator by the Python compiler, leading to unexpected behavior. It needs at least one actual `yield` or `await yield_()` expression.
- gotcha While `async-generator` supports returning non-`None` values from an `@async_generator` decorated function (similar to `return 'value'` in regular generators), native async generators introduced in Python 3.6 (PEP 525) do not support non-empty `return` statements and will raise a `SyntaxError`. This can cause confusion if mixing patterns.
Install
-
pip install async-generator
Imports
- async_generator
from async_generator import async_generator
- yield_
from async_generator import yield_
- asynccontextmanager
from async_generator import asynccontextmanager
- aclosing
from async_generator import aclosing
Quickstart
import asyncio
from async_generator import async_generator, yield_, asynccontextmanager, aclosing
@async_generator
async def my_async_generator(limit):
for i in range(limit):
await asyncio.sleep(0.01)
await yield_(i)
@asynccontextmanager
@async_generator
async def managed_resource():
print("\n[SETUP] Acquiring resource...")
resource = []
try:
await yield_(resource)
finally:
print("[TEARDOWN] Releasing resource.")
resource.clear()
async def main():
print("--- Using async generator ---")
async for item in my_async_generator(3):
print(f"Consumed: {item}")
print("\n--- Using async context manager ---")
async with managed_resource() as res:
res.append("data")
print(f"Resource in context: {res}")
print("Resource out of context.")
print("\n--- Generator cleanup with aclosing ---")
gen = my_async_generator(5)
async with aclosing(gen):
# Consume partially
await gen.__anext__() # Consume first item (0)
print("Partially consumed async generator, 'aclosing' will ensure cleanup.")
print("Generator closed via aclosing.")
if __name__ == '__main__':
asyncio.run(main())