← All articles
Python·October 1, 2026·6 min read

Python Generators and Lazy Evaluation, Explained

By Eduardo Dionisio

A generator is a function that produces values one at a time and pauses between them. Instead of returning a full list, it yields — and the difference in memory can be the difference between a script that runs and one that crashes.

yield pauses, return finishes

python
def countdown(n):
    while n > 0:
        yield n
        n -= 1

for x in countdown(3):
    print(x)   # 3, then 2, then 1

Calling countdown(3) does not run the body. It returns a generator object. Each next() resumes execution from the last yield, runs until the next yield, and pauses again — keeping only the current value in memory.

Why laziness wins

python
# Eager: builds a million-item list in memory
squares = [x * x for x in range(1_000_000)]

# Lazy: computes one square at a time
squares = (x * x for x in range(1_000_000))

# Or, with a generator function
for line in open('huge.log'):   # file objects are already lazy
    if 'ERROR' in line:
        print(line.strip())

A generator expression — note the parentheses instead of brackets — computes each square only when asked. It never holds the whole result in memory. The same idea powers itertools, file iteration, and a huge swath of the standard library.

The subtle parts

  • A generator can be consumed only once. If you need to iterate it twice, call the function again or wrap it in a list.
  • Generators are lazy, so errors surface at iteration time, not definition time.
  • yield from delegates to another generator — a clean way to compose them.