Generators
Learn how generator functions and yield let you produce values lazily, one at a time, without building an entire sequence in memory.
Introduction
In the previous lesson, you built an iterator by hand using a class with __iter__() and __next__(). It worked, but it took quite a bit of boilerplate code. Generators give you the exact same behavior — an object that produces values one at a time — with a fraction of the code.
Generators are one of Python's most elegant features. They let you describe a sequence of values using ordinary-looking function code, while Python automatically handles pausing, resuming, and remembering state for you.
- What a generator function is and how the yield keyword works.
- Why generators are lazy and memory-efficient.
- How a generator pauses and resumes its internal state.
- The difference between a generator expression and a list comprehension.
- A practical example of generating a large or infinite sequence lazily.
- How to convert a generator into a list when you need all the values at once.
What is a Generator?
A generator is a special kind of iterator that you create using a function containing the yield keyword instead of return. Calling a generator function does not run its code immediately — instead, it returns a generator object that follows the iterator protocol from the previous lesson.
Lazy
Values are produced only when requested, not all at once up front.
Stateful
A generator remembers exactly where it left off between calls.
Iterator-Compatible
A generator automatically implements __iter__() and __next__() for you.
Lightweight
Generators avoid holding an entire sequence in memory at once.
Generator Functions and yield
A generator function looks just like a normal function, except it uses yield to produce a value instead of return. Each time yield runs, it hands a value back to whoever is consuming the generator, and the function's execution freezes right there.
def count_up_to(limit):
number = 1
while number <= limit:
yield number
number += 1
counter = count_up_to(3)
print(type(counter))
print(next(counter))
print(next(counter))
print(next(counter))<class 'generator'>
1
2
3Notice that calling count_up_to(3) does not print anything by itself — it just creates a generator object. The code inside the function only runs as values are pulled out, one yield at a time, using next().
for number in count_up_to(5):
print(number)1
2
3
4
5How a Generator Pauses and Resumes
The key superpower of yield is that it remembers everything: local variables, the current position in the loop, and the point of execution. When next() is called again, the function continues right after the yield statement, as if it had never stopped.
def demo():
print("Starting")
yield 1
print("Resumed after first yield")
yield 2
print("Resumed after second yield")
yield 3
gen = demo()
print(next(gen))
print(next(gen))
print(next(gen))Starting
1
Resumed after first yield
2
Resumed after second yield
3This is very different from a regular function, which runs completely from top to bottom every single time it is called and forgets everything the moment it returns.
Why Generators Are Memory-Efficient
A regular function that builds and returns a full list must construct every single value in memory before handing anything back. A generator produces exactly one value at a time, uses a small constant amount of memory, and never needs to store the whole sequence.
def squares_list(n):
result = []
for i in range(n):
result.append(i * i)
return result # builds the ENTIRE list in memory
def squares_generator(n):
for i in range(n):
yield i * i # produces ONE value at a time
import sys
list_version = squares_list(1000000)
gen_version = squares_generator(1000000)
print(sys.getsizeof(list_version), "bytes")
print(sys.getsizeof(gen_version), "bytes")8448728 bytes
112 bytesThis is called lazy evaluation — values are computed only when they are actually needed, instead of all being computed and stored up front.
Generator Expressions
Just like list comprehensions have a compact syntax, generators have their own compact form called a generator expression. It looks almost identical to a list comprehension, but uses parentheses () instead of square brackets [].
list_comprehension = [x * x for x in range(5)]
generator_expression = (x * x for x in range(5))
print(list_comprehension)
print(generator_expression)
print(list(generator_expression))[0, 1, 4, 9, 16]
<generator object <genexpr> at 0x000001A2B3C4D5E0>
[0, 1, 4, 9, 16]The list comprehension immediately builds and stores all five values. The generator expression builds nothing yet — it only creates a generator object that will compute each value on demand, the same way a generator function does.
| Feature | List Comprehension | Generator Expression |
|---|---|---|
| Syntax | [x for x in ...] | (x for x in ...) |
| Memory | Stores all values at once | Produces one value at a time |
| Reusable? | Yes, any number of times | No, exhausted after one full pass |
| Best for | Small, reusable collections | Large or one-time-use sequences |
A Practical Example: An Infinite Sequence
Because generators produce values lazily, they can represent sequences that are extremely large — or even infinite — without ever running out of memory. This would be impossible with a regular list.
def infinite_counter(start=1):
number = start
while True:
yield number
number += 1
counter = infinite_counter()
for _ in range(5):
print(next(counter))1
2
3
4
5The while True loop never actually finishes — but that is fine, because the generator only computes the next number when it is asked for one. You control how many values you take, using a for loop with a limit, itertools.islice, or simply calling next() as many times as you need.
Converting a Generator to a List
Sometimes you genuinely need all the values at once — for example, to sort them, index into them, or loop over them multiple times. In that case, you can pass a (finite!) generator into list() to collect every value into a real list.
def even_numbers(limit):
for n in range(limit):
if n % 2 == 0:
yield n
evens = list(even_numbers(10))
print(evens)
print(len(evens))[0, 2, 4, 6, 8]
5Calling list() on an infinite generator like infinite_counter() will run forever and eventually crash your program — only convert generators to lists when you know they are finite.
Common Mistakes
- Calling list() on an infinite or extremely large generator, which can hang or crash your program.
- Trying to loop over the same generator twice — once exhausted, a generator stays empty; you must create a new one.
- Mixing up return and yield inside a generator function — using return simply ends the generator early (it does not "return" a value to the caller the way a normal function does).
- Forgetting that a generator expression does not create the values immediately, then being surprised that printing it shows a generator object instead of the results.
Best Practices
- Use a generator whenever you are processing a large sequence but only need one value at a time (for example, reading huge files line by line).
- Prefer a generator expression over a list comprehension when you are just going to loop over the result once and discard it.
- Only call list() on a generator when you are certain it is finite and reasonably sized.
- Give generator functions clear, descriptive names, just like regular functions — they are still functions, just special ones.
- Use generators to keep code readable while avoiding the memory cost of building large intermediate lists.
Frequently Asked Questions
Is a generator the same as an iterator?
A generator is a specific, convenient way to create an iterator. Every generator automatically follows the iterator protocol (__iter__ and __next__), so every generator is an iterator, but not every iterator is written as a generator.
Can a generator function have multiple yield statements?
Yes. Each time next() is called, execution resumes and runs until it hits the next yield statement, which can be a different one each time depending on your logic.
What happens if I use return inside a generator function?
A bare return simply stops the generator, causing the next call to next() to raise StopIteration. return value inside a generator attaches that value to the StopIteration exception, which is an advanced, rarely used feature.
Are generator expressions faster than list comprehensions?
Not necessarily faster per element, but they use far less memory since they never build the whole collection at once — this matters most for large sequences.
Can I restart a generator after it is exhausted?
No. Once a generator has yielded its last value, it is permanently exhausted. To iterate again, call the generator function (or write the generator expression) again to create a brand-new generator object.
Key Takeaways
- A generator function uses yield to produce values one at a time instead of building an entire result up front.
- Calling a generator function returns a generator object; the function body only runs as values are requested.
- A generator pauses at each yield and resumes exactly where it left off on the next call to next().
- Generators are memory-efficient because of lazy evaluation — they compute values on demand rather than storing them all.
- A generator expression (x for x in ...) is the compact, lazy counterpart to a list comprehension.
- Use list() to collect a finite generator's values, but never do this on an infinite generator.
Summary
Generators let you write code that produces sequences of values lazily, using the same simple function syntax you already know, plus the yield keyword. They avoid the memory cost of building huge lists, and they can even represent infinite sequences safely.
In this lesson, you learned how generator functions work, how they pause and resume, how generator expressions compare to list comprehensions, and when to convert a generator into a list. Next, you will learn about decorators, which let you modify the behavior of functions without changing their code.
- You understand what a generator function is and how yield works.
- You know why generators are memory-efficient thanks to lazy evaluation.
- You can write and use generator expressions.
- You are ready to learn about decorators.