Skip to content
datarekha
Python Medium Asked at GoogleAsked at AmazonAsked at DatabricksAsked at Spotify

How does yield differ from return, and what happens to a function's state when it yields?

The short answer

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

return tears down the function’s stack frame and hands one value back — locals gone, done. yield does something stranger and more powerful: it pauses the frame — every local, the exact instruction pointer, all of it — hands a value out, and resumes from precisely that spot on the next next() call. A function with yield in it is no longer a normal function; it’s a generator factory, and calling it runs none of the body — you get a generator object back, and execution begins only on the first next().

A worked example

The clearest way to feel the pause-and-resume is to print from inside the generator and watch when each line runs:

def countdown(n):
    print(f"[generator started, n={n}]")
    while n > 0:
        print(f"[about to yield {n}]")
        yield n
        print(f"[resumed, decrementing n={n} -> {n-1}]")
        n -= 1
    print("[generator exhausted]")

gen = countdown(3)
print("Generator created - no code has run yet")
print()

v = next(gen); print(f"Got: {v}"); print()
v = next(gen); print(f"Got: {v}"); print()
v = next(gen); print(f"Got: {v}"); print()

# A for loop / list() just drives next() until StopIteration
def squares_up_to(limit):
    n = 1
    while n * n <= limit:
        yield n * n
        n += 1

print("Squares up to 30:", list(squares_up_to(30)))

# yield from delegates straight to a sub-iterable
def chain(*iterables):
    for it in iterables:
        yield from it

print("Chained:", list(chain([1, 2], [3, 4], [5])))
Generator created - no code has run yet

[generator started, n=3]
[about to yield 3]
Got: 3

[resumed, decrementing n=3 -> 2]
[about to yield 2]
Got: 2

[resumed, decrementing n=2 -> 1]
[about to yield 1]
Got: 1

Squares up to 30: [1, 4, 9, 16, 25]
Chained: [1, 2, 3, 4, 5]

Trace the first few lines and the model snaps into focus. Creating the generator printed nothing. The first next() ran the body up to the first yield and stopped. The second next() resumed after that yield — printing the “resumed” line — looped once, and paused again. The frame survived untouched between calls; that survival is the entire difference from return.

Two-way communication with .send()

A generator can also receive a value at the point where it paused — that’s what .send() is for:

def accumulator():
    total = 0
    while True:
        value = yield total      # send total out; receive the next value in
        if value is None:
            break
        total += value

acc = accumulator()
next(acc)        # prime: run up to the first yield
acc.send(10)     # -> 10
acc.send(5)      # -> 15

return vs yield, side by side

def regular():   return 42       # exits, frame discarded
def gen_func():  yield 42         # pauses, frame saved

type(regular())   # int
type(gen_func())  # generator

A single yield anywhere in the body changes the function’s entire execution model — from “run once and return” to “pause and resume.”

Learn it properly Generators

Keep practising

All Python questions

Explore further