skip to content

Why are generator-based consumer coroutines wrapped in a priming decorator?

level: middleimportance: should knowfreq 42%

answer

  1. Generator bodies start lazily
  2. Not parked at a yield yet
  3. Sending a value first is an error
  4. Move the one-line fix into the definition
  5. Wrapper advances once, returns the generator

basics

~10 s

A newly created generator has not reached its first yield, so feeding it a value raises TypeError. A priming decorator calls the generator function, advances the object once, and returns it ready to receive.

solid answer

~50 s

Calling a generator function runs none of the body - it returns a generator object whose frame has not started, so it is not yet parked at any `yield` and cannot accept a value. Feeding one immediately raises `TypeError: can't send non-None value to a just-started generator`. The fix is mechanical, so the pattern is to hide it: a decorator wraps the generator function, calls it, advances the resulting object once, and hands back the started generator. Callers then treat the decorated name as a factory that returns something immediately usable, and can never forget the setup step. Use `functools.wraps` so the name and docstring survive. It is a convention, not a language feature - nothing in Python primes generators for you - and the cost is that any setup code before the first `yield` now runs at call time.

code

python · 13 lines
python
def consumer():
    while True:
        item = yield
        print("got", item)

c = consumer()
try:
    c.send("row-1")
except TypeError as exc:
    print("TypeError:", exc)

next(c)
c.send("row-1")

go deeper

for a junior

Remember that calling a generator function runs none of its body. Be ready to say that a consumer generator must be advanced once before it can be given a value, and that the decorator does that for you.

for a middle

Explain the mechanics end to end: the wrapper calls the generator function, resumes the object once so it parks at the first yield, returns it, and uses functools.wraps to keep the identity of the original function.

for a senior

Show the costs you have hit in real code: setup before the first yield now runs eagerly, the factory hands back live stateful objects, and double-priming injects a phantom None. Say when a plain object with a write method is the better call.

for a principal

Own the convention. Because there is no standard priming decorator, every codebase invents its own name for it; decide whether this shape is allowed at all, and if so where the single implementation lives, so readers meet one spelling instead of five.

### The problem the decorator solves A generator function does not execute when called. It constructs a generator object whose frame is created but not started, and only the first resume runs any of the body. For a *source* generator that is invisible and useful - it is the laziness everyone wants. For a *consumer* generator, one written as `while True: item = yield`, it is a trap: the object is not suspended at the `yield` yet, because it has not reached it. The only legal first resume is one that carries no value, and CPython says so explicitly: `TypeError: can't send non-None value to a just-started generator`. So every consumer generator has an obligatory setup step before it is usable. Leaving that to callers means the same line appears at every construction site, and the failure mode when someone forgets is an exception in production code that looks nothing like 'you forgot to start it'. ### What the decorator does The decorator wraps the generator *function*. The wrapper calls it to obtain the generator object, resumes it once so the body runs up to and parks at the first `yield`, and returns that started object. From the caller's point of view the decorated name is now a factory for ready-to-feed consumers, and the setup step cannot be forgotten because it is no longer at the call site. Apply `functools.wraps` to the wrapper so the original `__name__`, `__doc__` and module survive for tracebacks and introspection. This decorator is a convention with no stdlib home. You will see it under many names across codebases; there is no canonical import for it, which is itself worth saying in an interview, because candidates sometimes assert Python primes generators automatically or that a standard decorator exists for it. ### What it changes about the contract Three consequences are worth naming. First, **the decorated function's return type changes meaning**: it is no longer 'a generator you must start', it is 'a live, running consumer'. That is the point, but it also means the object is stateful from birth, and passing one to two different owners is now a shared-mutable-state bug. Second, **anything before the first `yield` now runs eagerly**, at call time rather than on first use. If the body opens a file, acquires a connection or validates arguments before its first `yield`, that work has silently moved earlier. Usually this is what you want for a consumer - it surfaces a bad argument at construction instead of at the first push - but it must be a deliberate choice. Third, **priming twice is not harmless**. A second bare resume does not re-start anything; it runs the body from the first `yield` to the next one with `None` as the received value, so a consumer that appends what it receives gets a spurious `None` in its buffer. Code that both uses the decorator and defensively advances the generator again at the call site is a real bug, not belt-and-braces. ### Alternatives You do not have to use a generator at all. An ordinary object with a `write()` method and, say, a `flush()` method carries exactly the same state, needs no priming, is trivially testable, and produces stack traces that a reader can follow. In modern async code the same shape is an `async def` consumer draining a queue. Choose the generator version when suspension really is the point - when the consumer's logic is a linear narrative with state between pushes that would otherwise become an explicit state machine, which is precisely the readability argument that made generators feel like coroutines in the first place. ### What to say in an interview The compact answer is: generator bodies are lazy, so a consumer is not listening yet when you create it, and the decorator moves the one-line fix out of every call site and into the definition. Then add the cost - eager pre-`yield` code and a live object handed to the caller - to show you have used it rather than read about it.

  • What goes wrong if a caller also advances a generator that the decorator already primed?
    The extra resume is not a no-op. It runs the body from the first `yield` to the next one with `None` as the received value, so a consumer that appends or accumulates what it is given records a phantom `None`. Nothing raises, so it shows up later as one bad record or an off-by-one count. Priming belongs in exactly one place: either the decorator or the call site, never both.
  • Does the priming decorator change when setup code inside the generator runs?
    Yes, and deliberately. Everything before the first `yield` - argument validation, opening a destination, building a buffer - now runs when the factory is called rather than on the first push. For a consumer that is usually an improvement, because a bad argument fails at construction. But it removes the laziness people expect from generators, so heavy setup should be reconsidered rather than silently moved earlier.
  • How would you write the same consumer without any generator at all?
    As a small object with a `write()` method and whatever state it needs as attributes, plus an explicit method to finish up. It needs no priming, can be inspected and unit-tested directly, and its tracebacks point at ordinary method frames. Prefer the generator only when the consumer's logic reads as one linear narrative with state between pushes that would otherwise turn into an explicit state machine.

saying these in an interview costs you the question

  • Thinks a new generator is already suspended at its first yield
  • Says Python primes consumer generators automatically
  • Claims a standard library decorator does the priming
  • Uses the decorator and still advances the generator at the call site
  • Believes the generator body runs when the function is called
  • Forgets functools.wraps and loses the name in tracebacks

context