Python Generators Explained
Generators produce values one at a time instead of building a full list in memory — what yield actually does, and when it matters
A generator function looks almost identical to a normal one, with one difference — yield instead of return — that changes its entire execution model.
What yield actually does
A normal function runs top to bottom and returns once. A generator function pauses at each yield, hands back a value, and remembers exactly where execution stopped. The next call to next() on it resumes right after that yield, continuing with all local state intact, rather than starting the function over.
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
for number in count_up_to(5):
print(number) # 1, 2, 3, 4, 5 — computed one at a time
Calling count_up_to(5) doesn’t run any of the function’s code yet — it returns a generator object. Code only actually runs as you iterate it (via a for loop, next(), or anything else that consumes an iterator), one yield at a time.
Why this matters: memory
The core benefit is that a generator never holds the full sequence in memory. [i for i in range(10_000_000)] builds a ten-million-element list immediately. (i for i in range(10_000_000)) — a generator expression, the same syntax with parentheses instead of brackets — produces values one at a time as they’re consumed, using a small constant amount of memory regardless of how large the sequence is. This also makes genuinely infinite sequences possible, since nothing needs to be fully computed upfront.
Where generators show up
Reading a large file line by line without loading the whole thing into memory, processing a stream of data (API pagination, a database cursor, a real-time feed), and building data pipelines where each stage transforms items one at a time rather than materializing intermediate lists — generator expressions and itertools lean on this heavily. Async generators (async def with yield) extend the same pattern to asynchronous iteration.
The trade-off to know
A generator can only be iterated once — once exhausted, it’s done, and there’s no rewinding. If you need to iterate the same data multiple times, or need random access (indexing, len()), a list is the right tool; a generator is specifically for sequential, one-pass, memory-conscious iteration. Reach for one when you’re processing something large or unbounded and don’t need every value held in memory at once.
Frequently Asked Questions
What is a generator in Python?
A function that uses yield instead of return, producing a sequence of values one at a time (lazily) instead of computing and returning them all at once. Each call to next() runs the function until the next yield, then pauses there.
What’s the difference between yield and return?
return exits a function and hands back one value, ending execution. yield pauses the function, hands back one value, and remembers exactly where it left off — the next call resumes right after that yield statement rather than starting over.
Why use a generator instead of a list?
Memory. A generator produces values on demand instead of building the entire sequence in memory upfront — critical for large or infinite sequences, or when you only need to process items one at a time and never need them all in memory simultaneously.
Can you use a generator twice?
No — a generator is exhausted after it’s fully iterated once. Calling it again requires creating a new generator (calling the generator function again), not re-iterating the same object.