Q1What's the closest match for print(type(gen)) here?def f():
yield 1
yield 2
gen = f()
print(type(gen))
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)
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.
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.
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)
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.
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.
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 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.
Knowledge Check
Answer each question one by one.
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))