How does an `async def` containing `yield` differ from a coroutine function?
answer
- One keyword changes what the compiler emits
- Calling it runs nothing at all
- Not awaitable — iterate it instead
- Bare return only, never a value
- PEP 525 async generator object
basics
~20 sIt is an async generator function: calling it returns an async generator object, not a coroutine, and runs no code. You never await that object — you drive it with async for or anext(), and it may not return a value.
solid answer
~50 sA `yield` anywhere in an `async def` body changes what the compiler produces: instead of a coroutine function you get an async generator function (PEP 525, Python 3.6). Calling it builds an async generator object and executes none of the body; the object has no `__await__`, so awaiting it is a `TypeError`. What it does have is `__aiter__` and `__anext__`, so `async for` and `anext()` drive it, each resume running the body up to the next `yield`, and falling off the end raises `StopAsyncIteration`. The point of the hybrid is that the body may both `await` and `yield` — fetch the next page, hand out its records, repeat — which a plain generator cannot do and a coroutine cannot do incrementally. Two rules the compiler enforces: `return value` is a `SyntaxError` here, and `yield from` is not allowed in any `async def`.
code
python · 11 linesimport inspect
async def stream():
yield 1
print(inspect.isasyncgenfunction(stream)) # True
print(inspect.iscoroutinefunction(stream)) # False
obj = stream()
print(hasattr(obj, "__anext__")) # True
print(hasattr(obj, "__await__")) # Falsego deeper
Recall the rule: a yield in an async def makes an async generator, and you consume it with async for rather than await. Be able to say that calling it runs none of the body.
Explain the mechanics — the object exposes __aiter__/__anext__, each resume runs to the next yield, exhaustion raises StopAsyncIteration — and name the compiler rules: no return value, no yield from.
Demonstrate the design judgment: when to stream with an async generator versus return a batch, why the frame staying alive between yields makes cleanup your problem, and why one object cannot serve concurrent consumers.
Own the contract you publish. Returning an async iterator makes back-pressure implicit and consumption sequential; be ready to weigh that against a queue hand-off or batched calls, and to say what the shape costs callers who are not async.
### Two different things that both start with `async def` An `async def` whose body contains no `yield` is a **coroutine function**: calling it builds a coroutine object that you `await` exactly once for exactly one result. Put a `yield` anywhere in that body and the compiler produces something else — an **async generator function**, introduced by PEP 525 in Python 3.6. Calling *that* returns an **async generator object** and runs none of the body. The object is not awaitable; it has no `__await__`, so `await stream()` raises `TypeError`. What it does have is `__aiter__` and `__anext__`, which makes it an async iterator, and the way values come out is `async for` or `anext()`. The standard library will tell the two apart for you: `inspect.isasyncgenfunction` is true for the generator function and `inspect.iscoroutinefunction` is false for it. ### Why the hybrid exists A synchronous generator can pause at a `yield` but cannot await. A coroutine can await but produces one result at the end. The async generator is the union of the two: the body may `await` — for the next page of a feed, the next chunk of a body, the next message — *and* `yield` each item as it becomes available. That is why nearly every "results as they arrive" API in async Python is shaped like one. A differ that walks a paginated schedule feed awaits each page request and yields the records inside it, and its consumer writes a four-line `async for` that never mentions pagination. ### Execution model Calling the function creates the object with its frame paused before the first statement. Each awaited `__anext__` — whether it comes from `async for` or from `anext()` — resumes the frame, runs until the next `yield`, and delivers that value to the consumer. When the body returns, `__anext__` raises `StopAsyncIteration`. Between yields the frame stays alive and holds its locals, its open `async with` blocks and its `try`/`finally` blocks; that persistence is the whole reason abandoning a stream halfway needs deliberate cleanup. ### The three driving methods Beyond plain iteration, an async generator object exposes three methods, and all three return awaitables: * `asend(value)` resumes the generator and makes `value` the result of the paused `yield` expression, then returns the next yielded value. The first resume must be `anext()` or `asend(None)` — there is no paused `yield` expression yet to receive anything. * `athrow(exc)` raises `exc` at the suspension point, so the generator's own `try`/`except` sees it. * `aclose()` throws `GeneratorExit` at the suspension point and runs the generator's cleanup. ### Rules the compiler enforces ```python try: compile("async def g():\n yield 1\n return 5\n", "<demo>", "exec") except SyntaxError as exc: print(exc.msg) # 'return' with value in async generator ``` A bare `return` is fine and simply ends the stream, but there is no return-value channel out of an async generator: a final summary has to be yielded as a last item or written to an object the caller supplied. `yield from` is rejected in any `async def`, so delegating to another async generator is spelled explicitly as `async for item in inner(): yield item`. ### One consumer at a time An async generator object is a single suspended frame, so two tasks cannot pull from it concurrently. The second `__anext__` while the first is still in flight raises `RuntimeError: anext(): asynchronous generator is already running`. Fan-out is done either by giving each consumer its own generator object, or by having one task drain the generator and hand items to workers through a queue — the loop can interleave the *consumers*, but never two pulls on the same object. ### It needs a running loop, and deliberate cleanup Because the body awaits, an async generator can only be driven inside a running event loop; there is no synchronous way to pull one item out. And because a consumer can walk away mid-stream, the `finally` clauses in the body are not guaranteed to run at the moment the consumer stops — which is why `contextlib.aclosing` exists and why any async generator that owns a resource should be written with its closing in mind from the first line. ### Typing and interop Annotate the function with `collections.abc.AsyncIterator[Item]` when consumers only iterate — it is the smallest promise that supports `async for` — and with `collections.abc.AsyncGenerator[Item, None]` when the caller is meant to use `asend` or `athrow` as well. Those ABCs are subscriptable directly, so `typing.AsyncIterator` is no longer needed for the annotation. The runtime checks are `inspect.isasyncgen` for an object and `inspect.isasyncgenfunction` for the function that makes them.
- How do you push a value back into a running async generator?Await its `asend(value)` method: the generator resumes with `value` as the result of the paused `yield` expression and returns the next yielded value. The very first resume must be `anext()` or `asend(None)`, because the frame has not reached a `yield` yet. `athrow()` raises an exception at the same point, and `aclose()` throws `GeneratorExit` there to trigger cleanup.
- What happens if two tasks pull from the same async generator object at once?The second one gets `RuntimeError: anext(): asynchronous generator is already running`. The object is one suspended frame, so only one resume can be in flight. Give each consumer its own generator object, or let a single task drain it and distribute the items — sharing the object between concurrent consumers is never safe.
- Can an async generator return a value to its consumer?No. `return value` inside one is a `SyntaxError`; only a bare `return` is allowed, and it just ends the stream. Unlike a synchronous generator, whose return value rides on `StopIteration`, there is no such channel here. Yield the summary as a final item, or write it into an object the caller passed in.
saying these in an interview costs you the question
- Awaits the object returned by an async generator function
- Says an async generator function returns a coroutine
- Uses `yield from` to delegate inside an `async def`
- Returns a value from an async generator to pass a result out
- Shares one async generator object between concurrent tasks
- Thinks the body starts running when the function is called