skip to content

yield and Suspended State

Calling a generator function runs none of its body — you get a generator object, and the first next() runs up to the first yield. The frame stays alive between yields, so locals survive.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does calling a Python generator function return, and when does its body first run?

level: juniorimportance: must knowfreq 78%

answer

  1. Two different things share one word
  2. The call produces no values
  3. Something must ask before anything runs
  4. First next() reaches the first yield
  5. GEN_CREATED until someone advances it

basics

~20 s

Calling it runs none of the body. You get back a generator object, which is an iterator. The body starts only on the first next() call and runs up to the first yield, then pauses there.

solid answer

~40 s

A `def` whose body contains `yield` anywhere is compiled into a **generator function**. Calling it binds the arguments and immediately hands back a **generator object** without executing a single statement of the body. That object is an iterator: it has `__iter__` and `__next__`. The first `next()` on it runs the body from the top until it reaches a `yield`, which produces that value and suspends execution in place; each later `next()` resumes at the statement after that `yield`. When the body ends, `StopIteration` is raised, which is what stops a `for` loop. The practical consequence is that every side effect in the body — logging, opening a file, validating a value — is deferred until somebody actually iterates, and never happens at all if nobody does.

code

python · 10 lines
python
def counter(limit):
    print("body started")
    n = 0
    while n < limit:
        yield n
        n += 1

gen = counter(3)
print("nothing printed yet:", type(gen).__name__)
print("first value:", next(gen))

go deeper

for a junior

Be ready to say plainly that the call gives you a generator object and runs nothing, and that the first next() runs up to the first yield. Practise pointing at a snippet and saying what has and has not executed.

for a middle

Explain the mechanics: the compiler flags the code object because yield appears in the body, arguments bind eagerly, the frame is created unstarted, and StopIteration is what a for loop catches to stop.

for a senior

Show that you reason about deferred side effects in real code — logging that never fires, resources opened late, exceptions whose traceback points at the consumer rather than the builder — and that you review callers when a yield is added to an existing function.

for a principal

Own the API-design angle: returning a lazily-started iterator instead of a materialized collection changes the contract for every caller, including error timing and resource lifetime. Decide deliberately which functions in a codebase are allowed to hand back laziness.

**"Generator function" and "generator object" are two different things**, and most confusion about `yield` starts by collapsing them into the single word "generator". ### The function is decided at compile time A generator function is a `def` whose body contains `yield` (or `yield from`) *anywhere* — even inside a branch that never executes. The compiler notices the keyword while compiling the body and flags the resulting code object as a generator; there is no decorator and no runtime check involved. This is why adding one `yield` to an ordinary function silently changes what every existing caller receives: ```python def load(rows): if not rows: return [] # this no longer returns a list to the caller yield rows[0] ``` After that edit, `load([])` returns a generator object that yields nothing, not `[]`. Nothing raises; callers that expected a list simply see an empty iteration. ### The call builds an object, not a result Calling a generator function does three things: it binds the arguments to parameters, it creates a frame for that call, and it wraps the frame in a generator object which it returns. It does **not** execute any statement of the body. Argument *binding* is the one eager step — `g()` on `def g(a): yield a` raises `TypeError` at call time, because binding happens before the generator object exists. Anything inside the body — a type check, a `print`, an `open`, a counter increment — waits. ### The object is an iterator The returned object is of type `types.GeneratorType`. It implements `__iter__` (returning itself) and `__next__`, which is the whole iterator protocol, so it can be used anywhere an iterable is accepted. `for value in gen:` is sugar for calling `iter()` on it and then `__next__()` repeatedly until `StopIteration` is raised, which the loop catches and turns into a normal exit. The first `next(gen)` transfers control into the frame at the first statement of the body and runs until a `yield` expression is evaluated. The value on the right of `yield` becomes the return value of `next()`, and execution **suspends** at exactly that point. The frame is not discarded; it is parked. The next `next(gen)` resumes on the line after the `yield`. This alternation continues until the body falls off the end or executes `return`, at which point `StopIteration` is raised and the generator is finished. ### Why deferred start matters in real code Because nothing runs until consumption, three things follow that trip people up: 1. **Side effects are lazy.** A generator function that logs "starting export" logs nothing when called. If the caller builds the generator and never iterates — say, an early `return` on an empty input list — the body never executes. 2. **Errors surface at the consumer.** An exception raised in the body propagates out of the `next()` call, so the traceback points at the loop, not at the line that created the generator. 3. **Partial consumption means partial execution.** `next(gen)` once on a ten-step body runs only up to the first `yield`; the remaining statements never happen unless iteration continues. ### Checking which one you hold The stdlib distinguishes the two directly: `inspect.isgeneratorfunction(f)` is true of the function, `inspect.isgenerator(g)` is true of the object it returns. `inspect.getgeneratorstate(gen)` reports `GEN_CREATED` for a generator that has been built but never advanced, and `GEN_SUSPENDED` once it is parked at a `yield` — a precise way to answer "has this body started yet?" during debugging. ### The mental model Calling the function writes down *how* to produce values; `next()` is what actually produces one. The generator object is a resumable call that has not started. Every question about generator behaviour — why a print did not appear, why a validation error came from the wrong place, why a counter is one behind — is answered by asking: has anyone called `next()` on it yet, and how many times? ### Driving it by hand versus a for loop Manual advancing makes the timing visible in a way a `for` loop hides: ```python def pair(): yield 1 yield 2 gen = pair() # nothing has run next(gen) # runs to the first yield next(gen) # resumes, runs to the second yield next(gen) # body ends -> StopIteration ``` The third call raises `StopIteration`, because the body reached its end. A `for` loop performs exactly these calls and catches that exception as its exit condition, which is why the laziness is invisible inside a loop: the loop calls `next()` immediately, so the body appears to start "straight away". The deferral only becomes observable when the generator object is created in one place and consumed in another — stored on an object, passed to a helper, or built inside an `if` that the consumer may skip. Exhaustion is also final. A generator that has raised `StopIteration` will keep raising it; it does not rewind. Iterating the same values again means calling the generator function a second time to get a fresh generator object.

  • If a generator function's body never runs at call time, when is a TypeError for a missing argument raised?
    At call time. Binding arguments to parameters happens before the generator object is created, so `g()` on `def g(a): yield a` raises `TypeError` immediately. Everything *inside* the body — including a manual type check on `a` — is deferred to the first `next()`. That split surprises people: some argument errors are eager, all body errors are lazy.
  • Does a generator expression like (x*2 for x in data) behave the same way as a generator function?
    Almost. It produces the same kind of generator object and the per-item work is equally lazy, but the *outermost* iterable is evaluated eagerly when the expression is created — `data` is looked up and `iter()` is called on it right then. So a `NameError` for `data`, or a `TypeError` for a non-iterable, fires at creation, while the body's work still waits for the first `next()`.
  • What happens if you add a yield to an existing function that returns a list?
    Every caller silently starts receiving a generator object instead of a list, including on the `return []` path, because the compiler classifies the whole function as a generator from the single `yield`. Nothing raises at the boundary; callers that index the result or call `len()` on it fail later, elsewhere. It is a change of return type, and it needs the callers reviewed.

Calling a generator function is like handing someone a recipe card, not a cooked dish. Nothing is cooked until they start following it, and they stop at each step marked "pause here".

saying these in an interview costs you the question

  • Says calling a generator function runs the body
  • Thinks a generator function returns a list
  • Believes the whole body runs on the first next()
  • Cannot say what makes a def a generator function
  • Expects prints or logging at call time
  • Confuses the generator object with the function itself

context

open as a page

How does a Python generator preserve its local variables across a yield?

level: middleimportance: must knowfreq 60%

basics

~20 s

The generator object owns the call's frame. A yield suspends that frame instead of destroying it, so locals and the resume point survive. Resuming continues in the same frame; the frame is released only when the generator finishes.

open as a page

What happens when a Python generator function executes a return statement?

level: middleimportance: should knowfreq 46%

basics

~20 s

It ends the generator by raising StopIteration rather than yielding anything. Any returned value is not produced as an item; it is attached to that StopIteration. A for loop catches the exception and simply stops.

open as a page

A metrics scraper's generator function validates its config, but the error only appears once the consumer iterates. Why, and how do you make it raise at call time?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because no statement of a generator function's body runs until the first next(), the validation is deferred to the consumer. Split the function: a plain function validates eagerly and returns the generator built by a private generator function.

open as a page