← 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 1Calling 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.