How does yield differ from return, and what happens to a function's state when it yields?
return exits the function and discards its local state. yield suspends execution, saves the entire stack frame (locals, instruction pointer), and resumes from exactly that point on the next next() call. A function containing yield becomes a generator factory rather than a regular function.
How to think about it
The key mental model
return tears down the stack frame and hands one value back. yield pauses the stack frame — all locals, the instruction pointer, everything — hands a value back, and then resumes from exactly that point on the next next() call. Think of it as a function that can be interrupted and continued.
Calling a generator function doesn’t execute its body at all — it returns a generator object. Execution starts only on the first next().
Step through a generator to see state preservation
The memory advantage
A list comprehension builds all values in memory at once. A generator produces values lazily — one at a time, on demand. For large datasets, this is the difference between loading everything and streaming:
# list — holds all 1M numbers in memory
squares = [x * x for x in range(1_000_000)]
# generator — holds only one number at a time
squares = (x * x for x in range(1_000_000))
Two-way communication with .send()
Generators can also receive values back from the caller using .send():
def accumulator():
total = 0
while True:
value = yield total # send total out; receive next value in
if value is None:
break
total += value
acc = accumulator()
next(acc) # prime: advance to first yield
acc.send(10) # -> 10
acc.send(5) # -> 15
Key insight — generator vs regular function
def regular(): return 42 # exits, frame gone
def gen_func(): yield 42 # pauses, frame saved
type(regular()) # int
type(gen_func()) # generator
The presence of yield anywhere in the function body transforms its entire execution model.