Generators produce a sequence of values one at a time instead of building a full list. A generator function uses yield; each yield sends a value to the caller and pauses until the next value is requested. Generator expressions (x for x in ...) are the lazy version of list comprehensions. They save memory and allow infinite or pipeline-style iteration. See Iterators and iterables for the protocol behind them.
What they are
When you call a generator function, it returns a generator object (an iterator). Each time the iterator is advanced (e.g. in a for loop or with next()), the function runs until the next yield, produces that value, and then pauses. No full list is built in memory.
Generator function: yield
def count_up_to(n):
count = 1
while count <= n:
yield count
count += 1
for num in count_up_to(5):
print(num) # 1, 2, 3, 4, 5
Compare to building a list: return list(range(1, n+1)) would create the whole list at once.
Generator expression
Use parentheses instead of square brackets for a lazy, one-at-a-time version of a list comprehension:
# List: all values in memory
squares_list = [x ** 2 for x in range(5)]
# Generator: values on demand
squares_gen = (x ** 2 for x in range(5))
for s in squares_gen:
print(s)
Memory and pipelines
Generators are useful when the sequence is large or unbounded. You can also chain them for pipelines (each step yields to the next).
def get_numbers(n):
for i in range(n):
yield i # One value at a time
# Infinite generator (use with care):
def count_forever():
n = 0
while True:
yield n
n += 1
When to use generators
- Large or unbounded data: avoid loading everything into a list.
- One-pass processing: read or transform items as you iterate.
- Pipelines: chain generator steps instead of building intermediate lists.
- Use a list comprehension when you need a real list (e.g. multiple passes or indexing).
Next steps: Core Concepts · Iterators and iterables · List comprehensions · Functions