Learn by reading through in order

Generator Functions with yield — Producing Values One at a Time to Save Memory

Walk through how Python generator functions stream values one at a time with yield, all runnable in your browser.

In the previous article you saw closures — how to seal state inside a function and return a different value on each call. Python has a dedicated mechanism for this kind of "return the next value on each call" function: the generator function. Use yield instead of return, and the function pauses partway through to return a value, then resumes from where it left off the next time it's called.

It's a great fit when you don't want to materialize a big collection all at once and would rather stream one value at a time — log processing, bulk data crunching, and similar workloads.

yield Basics — Returning One Value at a Time

A function with yield value in its body is a generator function. Calling it like a regular function doesn't run the body — you just get back a special generator object.

To actually pull a value out, call next(obj).

The first next() runs the body up to the first yield and returns that value.

The next next() resumes from there and runs to the next yield.

Once there are no more yields left, another next() raises StopIteration.

def simple():
    yield 1
    yield 2

gen = simple()
print(type(gen))    # <class 'generator'>

print(next(gen))    # 1
print(next(gen))    # 2
print(next(gen))    # 3
# print(next(gen))  ← StopIteration (no more yields)
How yield and next() Relate
gen = simple()(not yet running)generator object is creatednext(gen) → stops at yield 1 → returns 1next(gen) → stops at yield 2 → returns 2next() → no more yields, StopIteration1st time2nd time3rd onward

Difference from return

A regular function returns and the entire scope vanishes — the next call starts from the top. A generator function, by contrast, pauses on yield and keeps locals and the position around. The next next() resumes right where you left off, and that's the big difference.

Build a generator function that yields order IDs 1, 2, 3 in order, and pull them out one by one with next().

① Define def order_ids(): with three lines: yield 1 / yield 2 / yield 3.

② Build a generator object: gen = order_ids().

③ Call print(next(gen)) three times in a row and confirm 1 / 2 / 3 come out.

(When the answer is correct, the explanation will appear.)

Python Editor

Run code to see output

Pulling Values with for — No Need to Worry About StopIteration

Writing next() every time gets tedious, and you shouldn't have to handle StopIteration yourself either. Use for value in generator: instead, and Python calls next() under the hood and breaks out of the loop automatically when the generator is done. This is by far the more common form.

Pairing with a for Loop
for v in gen:Generator sideyield returns and pausesCaller sideLoop body uses the valueAuto-stopswhen yields run outnextvalue

If you sprinkle print("in progress...") lines on both sides, you can watch each yield toggle execution back and forth — generator side → caller side → generator side.

def count_up_to(max_value):
    print("generator starting")
    for i in range(max_value):
        print(f"  before yield: {i}")
        yield i
        print(f"  after yield: {i}")

for v in count_up_to(3):
    print(f"received: {v}")

# Output flow:
# generator starting
#   before yield: 0
# received: 0
#   after yield: 0
#   before yield: 1
# ... (continues)

Use for to receive customer data from a generator that streams one at a time.

① Define def each_customer(): and loop with for name in ["Alice", "Bob", "Charlie"]:, yielding name.

② Pull values with for name in each_customer(): and print f"Next customer: {name}".

If all three appear in order, you're done.

Python Editor

Run code to see output

Difference from list — Cutting Memory Use

When you're working with values from 0 to 999_999, a list comprehension materializes all 1,000,000 integers at once in memory. A generator, on the other hand, keeps only the current value and computes the next one on demand. The list ends up in the multi-MB range; the generator object itself is just a few hundred bytes.

list vs generator Memory Footprint
list[0, 1, 2, ..., 999999]Several MBloaded at onceGood when you needthe whole thinggenerator(i for i in range(...))A few hundred bytes(only current item)Good for streamingone at a time
import sys

MAX = 10 ** 6

# List: loads everything into memory at once
data_list = [i for i in range(MAX)]
print(sys.getsizeof(data_list))
# e.g. 8000056 (~8 MB)

# Generator expression: only the current item
data_gen = (i for i in range(MAX))
print(sys.getsizeof(data_gen))
# e.g. ~200 bytes

# Caller-side code is identical
for v in data_gen:
    if v > 2:
        break
    print(v)
# 0
# 1
# 2

The Generator Expression Shortcut

Swap the square brackets [ ... ] of a list comprehension for parentheses ( ... ) and you have a generator expression. (i for i in range(1_000_000)) gives you the same effect as a def-based generator function in a single line. Pass it straight to sum() / max() / any() and friends — it just works.

Build a generator expression (a list comprehension with parentheses instead of square brackets) for some price data, confirm the type with type(), then pull values one at a time with for.

① Build prices = (base * 100 for base in range(1, 6)). The trick is using ( ) instead of [ ].

② Print the type with print(type(prices)) and confirm <class 'generator'> shows up.

③ Pull values with for p in prices: and print f"Price: {p}".

Python Editor

Run code to see output

Chaining Generators with yield from

When you want a generator to forward values from another generator as-is, you can write yield from sub_generator in one line instead of for v in sub: yield v. It's handy when you want to merge multiple data sources into a single generator.

For instance, with a function that streams sales for the Tokyo branch and another for Osaka, lining up yield from tokyo_sales() and yield from osaka_sales() gives the caller a generator that looks like one continuous stream.

def tokyo_sales():
    yield 1200
    yield 980

def osaka_sales():
    yield 850
    yield 1340

def all_sales():
    yield from tokyo_sales()
    yield from osaka_sales()

for amount in all_sales():
    print(amount)
# 1200
# 980
# 850
# 1340
yield from Delegates to a Sub-Generator
Caller sidefor amountin all_sales()all_sales()maingeneratorOrder received1200 → 980→ 850 → 1340①yield fromtokyo_sales()tokyo_sales()yield 1200yield 980②yield fromosaka_sales()osaka_sales()yield 850yield 1340drivesdelegatenext when donedelegate

yield from sub_gen() is shorthand for for v in sub_gen(): yield v.

The values the sub yields go straight to the outer caller, so

tokyo_sales's 1200, 980,

then osaka_sales's 850, 1340

arrive at for amount in all_sales(): in order.

Build a generator that combines per-store inventory lists into a single stream.

① Define def store_a(): and for item in ["apple", "orange"]: yield item.

② Define def store_b(): and for item in ["banana", "grape", "strawberry"]: yield item.

③ Define def all_items(): and write yield from store_a() followed by yield from store_b().

④ Iterate with for item in all_items(): print(item) to print all 5 in order.

Python Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What's the closest match for print(type(gen)) here?
def f():
yield 1
yield 2
gen = f()
print(type(gen))

Q2What happens when you call next() on a generator after every yield has been used up?

Q3Which of these two lines uses dramatically less memory?
A: data = [i for i in range(10**6)]
B: data = (i for i in range(10**6))