async generators
async iteration
programming
Python
concurrency

Asking for examples of async generators not directly transformable into manually implemented async iteration

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Manual async iterators can reproduce simple for await behavior, but advanced async generator semantics are hard to replicate correctly. The difficulty is not basic yielding. The hard parts are cleanup, cancellation, and two way control methods such as asend and athrow.

Why Async Generators Are More Than __anext__

A minimal async iterator in Python only needs __aiter__ and __anext__. Async generators provide more protocol surface and stronger guarantees.

  • asend injects values into a paused generator.
  • athrow injects exceptions at suspension points.
  • aclose guarantees finalization behavior.
  • try and finally blocks run predictably when iteration stops early.

You can manually build equivalents, but direct transformation is rarely mechanical once these features matter.

Example 1: Early Stop With Deterministic Cleanup

Async generators naturally express resource ownership.

python
1import asyncio
2
3async def stream_numbers():
4    print("open resource")
5    try:
6        for i in range(5):
7            await asyncio.sleep(0.05)
8            yield i
9    finally:
10        print("close resource")
11
12
13async def main():
14    async for n in stream_numbers():
15        print("got", n)
16        if n == 2:
17            break
18
19
20if __name__ == "__main__":
21    asyncio.run(main())

When the loop breaks, finalization still runs. A hand written iterator class must implement equivalent close semantics explicitly, and many implementations forget it.

Example 2: Two Way Communication With asend

Async generators can receive data between yields.

python
1import asyncio
2
3async def accumulator():
4    total = 0
5    while True:
6        value = yield total
7        if value is None:
8            return
9        total += value
10
11
12async def main():
13    gen = accumulator()
14    first = await gen.asend(None)  # prime generator
15    print(first)
16
17    print(await gen.asend(3))
18    print(await gen.asend(4))
19
20    try:
21        await gen.asend(None)
22    except StopAsyncIteration:
23        pass
24
25
26if __name__ == "__main__":
27    asyncio.run(main())

Recreating this control flow with a manual iterator object is possible, but not direct. You need a custom state machine that handles input injection at exact suspension boundaries.

Example 3: Error Injection With athrow

Error channels are another area where async generators provide built in semantics.

python
1import asyncio
2
3async def worker():
4    try:
5        while True:
6            try:
7                item = yield "ready"
8                print("process", item)
9            except ValueError:
10                print("recover from value error")
11    finally:
12        print("worker finalized")
13
14
15async def main():
16    gen = worker()
17    print(await gen.asend(None))
18    await gen.asend("task-1")
19    await gen.athrow(ValueError("bad input"))
20    await gen.aclose()
21
22
23if __name__ == "__main__":
24    asyncio.run(main())

A manual iterator could emulate this, but only by reimplementing exception routing logic. That is where direct transformability breaks down in practice.

When Manual Iterators Still Make Sense

Manual async iterators are useful for simple adapters, especially when wrapping callback style APIs or C extensions. They are also fine when you only need one way pulling with very small state.

Use them when requirements are strict and narrow:

  • Only __anext__ is needed.
  • No bidirectional send.
  • No external exception injection.
  • Cleanup can be handled by explicit context manager boundaries.

For anything richer, async generators reduce custom protocol code and usually improve correctness.

Testing Strategy

For async generator behavior, unit tests should include:

  • normal iteration to completion,
  • early break and cleanup assertion,
  • cancellation path,
  • explicit athrow path if used,
  • repeated close calls to verify idempotency.

These tests catch subtle leaks and unfinished tasks that are common in hand rolled iteration logic.

Common Pitfalls

  • Assuming __anext__ alone covers all async generator semantics.
  • Forgetting deterministic cleanup when consumers stop early.
  • Recreating asend behavior with ad hoc mutable shared state.
  • Swallowing cancellation exceptions and leaking resources.
  • Building a manual state machine without tests for exception injection.

Summary

  • Async generators provide richer protocol guarantees than basic manual async iterators.
  • Cleanup, asend, athrow, and aclose are the main complexity points.
  • Direct mechanical transformation is rare once two way control is needed.
  • Manual iterators are still useful for narrow adapter scenarios.
  • Prefer async generators when correctness around lifecycle and cancellation matters.

Course illustration
Course illustration

All Rights Reserved.