Generators
Produce values lazily, one at a time, instead of building whole collections.
- Write a generator function with yield
- Explain lazy evaluation and why it saves memory
- Choose a generator over a list when appropriate
A generator produces values one at a time, on demand, instead of computing them all up front and storing them in a list. It's the complexity lesson in practice: generators trade nothing in time but turn O(n) memory into O(1).
yield makes a generator
A function that uses yield instead of return becomes a generator. Each
yield hands back one value and pauses, resuming where it left off when the
next value is asked for:
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
for x in count_up_to(3):
print(x) # 1, 2, 3count_up_to(3) doesn't run the loop immediately — it returns a generator that
produces each value as the for loop pulls it.
Lazy evaluation saves memory
A list comprehension builds the whole collection in memory; a generator expression (parentheses instead of brackets) produces items as needed:
sum([n * n for n in range(1_000_000)]) # builds a million-item list first
sum(n * n for n in range(1_000_000)) # streams one value at a time — no listBoth give the same answer; the second never holds more than one number at once. For large or infinite sequences, that's the difference between fine and out of memory.
When to use one
Reach for a generator when you're iterating once over a large (or unbounded) sequence and don't need it all in memory at once. If you need to index it, reuse it, or know its length, a list is the right call.
A generator is single-use: once exhausted, it's empty. If you need the values
again, either recreate the generator or materialise it with list(...).
Where to go next
Next: decorators — wrapping functions to add behaviour without changing them.