How does a generator used as a data source differ from one driven by its send() method?
answer
- Which side owns the loop?
- Pull versus push
- yield as statement versus expression
- send() supplies what yield evaluates to
- next() is send(None)
basics
~20 sA source generator is pulled: each next() runs it to the next yield and hands a value out. A send()-driven generator is the mirror image - it parks at a yield expression and the caller pushes values in.
solid answer
~40 sThey are the same kind of object wired in opposite directions. A source generator is *pulled*: `next()` runs the body to the next `yield` and hands the value out, and a `for` loop is just repeated `next()` until `StopIteration`. A consumer generator writes `item = yield` instead, treating `yield` as an expression: it suspends there and the caller *pushes* data in with `gen.send(item)`, and that expression evaluates to whatever was sent. So the driving side flips - with a source the consumer of the data owns the loop, with a consumer generator the producer does. That inversion is exactly what let generators stand in for coroutines before `async def` existed: a routine you can suspend, resume and feed, with its locals kept alive in the frame in between.
code
python · 17 linesdef rows():
for i in range(3):
yield {"id": i}
def sink():
written = 0
while True:
row = yield written
written += 1
for row in rows():
print("pulled", row)
s = sink()
s.send(None)
print("pushed, count =", s.send({"id": 0}))
print("pushed, count =", s.send({"id": 1}))go deeper
Recall that yield can appear on the right of an assignment. Be ready to say that a for loop pulls values out, while send() pushes a value in and that value becomes the result of the yield expression.
Explain the mechanics: next() is send(None), the generator's locals survive between resumptions in a suspended frame, and a for loop over a consumer generator silently binds None every time rather than raising.
Show judgement about which direction to reach for. A push-shaped consumer earns its place when the data source is a callback or event handler you do not drive; otherwise a pull generator or a plain object with a write method reads better to the next maintainer.
Own the guidance: push-shaped generators are clever and easy to misuse, so decide where they are allowed in the codebase at all and what the modern default is - an async consumer or an ordinary object - so the shape does not spread by imitation.
### One object type, two wiring directions Any `def` whose body contains `yield` is a generator function. Calling it runs none of the body; it builds a generator object. That object exposes two complementary interfaces at once, and which one you actually use is what makes a generator a *source* or a *consumer*. ### The source direction (pull) In the familiar shape, `yield value` is used as a statement. `next(gen)` resumes the frame, runs it until the next `yield`, and hands the yielded object back to the caller; when the body returns, `StopIteration` is raised. A `for` loop is nothing more than that call repeated until `StopIteration` arrives. Note who is in charge: the code that *wants* the data decides when the next item is produced. The generator is passive, frozen mid-frame until someone asks. This is the shape used to stream rows off disk or to compose lazy pipelines. ### The consumer direction (push) `yield` is an expression, not merely a statement, so a generator may write `item = yield`. Now the generator suspends *waiting to be given something*. The caller resumes it with `gen.send(item)`, and the suspended `yield` expression evaluates to that object. The generator then runs on until it reaches the next `yield`, and whatever that yield produces becomes the return value of `send()`. Control has inverted: the code that *has* the data decides when the generator runs, and the generator behaves as a sink with memory - it keeps a running total, a batch buffer, a parser state, whatever it accumulated on previous pushes, because its locals live in a suspended frame rather than being rebuilt on each call. ### next() is send(None) There is only one resume operation underneath. `next(gen)` is defined to be `gen.send(None)`. Two consequences follow. First, a plain `for` loop over a consumer generator is legal and silently wrong: every `item = yield` binds `None`, because a `for` loop has no way to supply a value. Second, a generator that has just been created is not yet suspended at any `yield` at all - its frame has not started - so the first resume must be `next()` or `send(None)`; sending a real value first raises `TypeError: can't send non-None value to a just-started generator`. That is why consumer generators are conventionally wrapped in a priming decorator that advances them once at creation. ### Both directions at once The two roles are not exclusive. `sent = yield produced` receives a value and hands one back on the same suspension point, which is how a stateful accumulator reports its running result to the pusher. It is legal, but a generator that is simultaneously a serious data source and a serious sink is usually harder to read than two objects, so most real code picks a direction and stays in it. ### Why the distinction is worth knowing Historically this inversion is the entire reason generators could serve as lightweight coroutines: a suspendable, resumable routine that can be fed. Practically, it tells you which shape to reach for. A source generator is the right answer when your code owns the iteration and wants values lazily. A consumer generator earns its place when the data arrives from something you do *not* drive - a callback, an event handler, a parser feeding tokens - because such a caller cannot be turned into a `for` loop over your pipeline; it can only push. In modern code the same push shape is usually written as an `async def` consumer reading from a queue, or as an ordinary object with a `write()` method, but the generator version is still what you will meet in older codebases and in interview questions about how `await` came to exist. ### Diagnosing it Two symptoms give the wiring away. A consumer generator whose body sees only `None` means someone iterated it instead of sending to it. A `TypeError` about a just-started generator means it was fed before it was advanced to its first `yield`.
- What happens if you run a plain for loop over a generator that expects values via send()?It runs, and it is silently wrong. A `for` loop can only call `next()`, which is `send(None)`, so every `item = yield` inside the generator binds `None` on each pass. Nothing raises; you just get a consumer processing a stream of `None`. If the generator also yields values, the loop collects those, which can make the bug look like partially working code.
- Can one generator both receive a value and hand one back at the same suspension point?Yes. `sent = yield produced` does both: the object on the right of `yield` becomes the return value of the caller's `send()`, and the value the caller passes to the next `send()` becomes the result of the expression. A running accumulator uses this to report its current total to whoever is feeding it. It is legal and occasionally elegant, but mixing a real data source and a real sink in one generator is usually less readable than two separate objects.
A source generator is a vending machine: it does nothing until you press for the next item. A send()-driven generator is a mail slot: it sits open and does its work each time you post something through it.
saying these in an interview costs you the question
- Thinks yield can only send values out, never receive
- Says next() and send(None) do different things
- Claims a for loop can feed values into a generator
- Expects gen.send(x) to return x back to the caller
- Believes the generator body runs when the function is called