A generator function uses `yield` instead of `return` to produce a sequence of values lazily, pausing state between each call.
python
def block_numbers(start, count):
n = start
for _ in range(count):
yield n
n += 1
gen = block_numbers(100, 3)
list(gen) # [100, 101, 102]Generators implement the iterator protocol (`__iter__`/`__next__`) automatically, and can only be consumed once. They're ideal for streaming large or infinite sequences without holding everything in memory.
`next(gen)` retrieves the next value, raising `StopIteration` when exhausted (loops handle this automatically).