Skip to content

Generators

A generator is a function that can pause its execution and resume from where it left off. Instead of computing all values and returning them in a list, a generator produces values one at a time — only when asked. This is called lazy evaluation.

The key ingredient is the yield keyword. When Python encounters yield, it pauses the function, hands the value to the caller, and freezes the local state (variables, the instruction pointer). The next call to next() resumes from that exact line.

def count_up(start: int, stop: int):
current = start
while current <= stop:
yield current # pause here and emit current
current += 1 # resume here on the next next() call
gen = count_up(1, 4)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
print(next(gen)) # 4
# next(gen) would now raise StopIteration
AspectRegular functionGenerator function
ReturnsOnce, with returnMany times, with yield
StateDiscarded after returnFrozen between yields
MemoryBuilds full resultOne value at a time
TypeThe return valueA generator object

A function becomes a generator function the moment it contains even one yield statement. Calling it does not execute any code — it returns a generator object immediately.

Every generator object implements the iterator protocol: it has __iter__() and __next__() methods.

  • next(gen) advances to the next yield and returns that value.
  • When there are no more values, next(gen) raises StopIteration.
  • A for loop calls next() automatically and catches StopIteration to stop.
def three_words():
yield "Python"
yield "is"
yield "expressive"
# Equivalent — for loop handles next() and StopIteration for you
for word in three_words():
print(word)

Just as list comprehensions build lists, generator expressions build generators — using parentheses instead of brackets. They are memory-efficient because no list is materialised.

squares_list = [x ** 2 for x in range(1_000_000)] # allocates a list
squares_gen = (x ** 2 for x in range(1_000_000)) # allocates almost nothing
# Consume lazily
for sq in squares_gen:
if sq > 25:
break

Generator expressions compose naturally with built-ins that accept iterables:

total = sum(x ** 2 for x in range(10)) # no brackets needed inside sum()

Because generators are lazy, you can define sequences that never end — as long as the consumer stops asking.

def fibonacci():
a, b = 0, 1
while True: # infinite loop — but never blocks
yield a
a, b = b, a + b
from itertools import islice
def fibonacci():
"""Infinite Fibonacci sequence generator."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Take only the first 10 values — safe because islice stops early
first_10 = list(islice(fibonacci(), 10))
print("First 10 Fibonacci numbers:")
print(first_10)
# Generator expression: squares of even numbers
even_squares = (x * x for x in range(1, 11) if x % 2 == 0)
print("\nSquares of even numbers 1-10:")
for val in even_squares:
print(val, end=" ")
print()
# Demonstrate single-use nature
gen = (x * 3 for x in range(5))
print("\nFirst pass:", list(gen))
print("Second pass:", list(gen)) # exhausted — empty list
# Manual next() / StopIteration
counter = iter(range(3))
print("\nManual iteration:")
print(next(counter))
print(next(counter))
print(next(counter))
try:
print(next(counter))
except StopIteration:
print("StopIteration raised — generator exhausted")
What happens when Python encounters `yield` inside a function?
What exception signals that a generator has no more values to produce?
What is the main advantage of a generator expression over a list comprehension?
After iterating a generator to exhaustion, what does iterating it again produce?