Python Generators
Learn how Python can produce values one at a time using
yield. Generators provide a powerful,
memory-efficient way to work with large datasets,
streams, sequences, and custom iteration logic.
Learning Objectives
By the end of this lesson, you will understand how generators work and why they are extremely useful when processing data efficiently.
- Understand generator functions.
-
Use the
yieldkeyword. -
Retrieve generator values with
next(). -
Use generators with
forloops. -
Understand the difference between
yieldandreturn. - Understand generator state.
- Understand lazy evaluation.
- Build memory-efficient data processing pipelines.
- Create generator expressions.
-
Understand advanced generator methods such
as
send(),close(), andthrow().
What Is a Generator?
A generator is a special type of iterator that produces values one at a time instead of creating all values at once.
Generators are created using functions containing the
yield keyword.
def numbers():
yield 1
yield 2
yield 3
for number in numbers():
print(number)
1
2
3
Unlike a normal function, the generator does not execute completely when it is called. It produces each value when requested.
The yield Keyword
The yield keyword is what makes a function
a generator function.
def generate_numbers():
yield 10
yield 20
yield 30
Each yield produces one value.
When Python reaches a yield, execution is
temporarily paused. When the generator is requested
again, execution continues from that point.
The generator gives a value to the caller, remembers where it stopped, and continues from that exact location later.
Generator Functions
A function containing yield becomes a
generator function.
def countdown():
yield 3
yield 2
yield 1
generator = countdown()
print(generator)
Calling the function does not immediately execute all of its code.
Instead, Python creates a generator object.
Using next()
You can manually retrieve values from a generator using
the built-in next() function.
def numbers():
yield 10
yield 20
yield 30
generator = numbers()
print(next(generator))
print(next(generator))
print(next(generator))
10
20
30
After the final value has been produced, another
next() call raises
StopIteration.
Generators with for Loops
In normal Python programs, generators are often consumed
with for loops.
def even_numbers():
for number in range(2, 11, 2):
yield number
for number in even_numbers():
print(number)
2
4
6
8
10
The for loop automatically requests values
from the generator until the generator is exhausted.
yield vs return
Understanding the difference between these two keywords is essential.
| return | yield |
|---|---|
| Ends the function. | Pauses the generator. |
| Normally produces one final result. | Can produce many values. |
| Does not preserve execution state. | Preserves generator state. |
| Used by normal functions. | Used to create generators. |
def get_number():
return 10
# Code after return
# will not execute
def get_numbers():
yield 10
yield 20
yield 30
Generator State
One of the most powerful features of generators is that they remember their execution state.
def demo():
print("Step 1")
yield 100
print("Step 2")
yield 200
print("Step 3")
yield 300
generator = demo()
print(next(generator))
print(next(generator))
print(next(generator))
Step 1
100
Step 2
200
Step 3
300
Notice that the generator does not restart from the
beginning after every next() call.
Memory Efficiency
One of the biggest advantages of generators is their ability to produce values lazily rather than storing an entire sequence in memory.
Consider generating one million numbers.
numbers = [number for number in range(1000000)]
The list stores all one million values.
numbers = (number for number in range(1000000))
The generator produces values when they are requested.
Processing Large Data
Imagine processing millions of records from a data source.
Loading everything into a list may require a large amount of memory.
A generator can instead produce records one at a time.
def generate_orders():
for order_id in range(1, 1000001):
yield {
"order_id": order_id
}
for order in generate_orders():
if order["order_id"] == 5:
print(order)
The generator does not need to create a million dictionaries before processing begins.
Generators are useful when processing large files, logs, database records, API responses, streams, and data pipelines.
Generator Expressions
Python provides a shorter syntax for creating simple generators called a generator expression.
The syntax looks similar to a list comprehension, but uses parentheses instead of square brackets.
numbers = (number * 2 for number in range(5))
for number in numbers:
print(number)
0
2
4
6
8
The expression produces values lazily.
List vs Generator
| List | Generator |
|---|---|
| Stores values. | Produces values. |
| Usually uses more memory. | Usually uses less memory. |
| Can be indexed directly. | Does not normally support direct indexing. |
| Can be reused. | Usually consumed once. |
| Good for repeated access. | Good for sequential processing. |
Lazy Evaluation
Generators use a concept called lazy evaluation.
Lazy evaluation means that a value is calculated only when it is needed.
def numbers():
print("Generating 1")
yield 1
print("Generating 2")
yield 2
print("Generating 3")
yield 3
generator = numbers()
print("Generator created")
print(next(generator))
print(next(generator))
Generator created
Generating 1
1
Generating 2
2
Notice that the third value is never generated because it was never requested.
send()
Advanced generators can receive values from the caller
using send().
def receiver():
while True:
value = yield
print("Received:", value)
generator = receiver()
next(generator)
generator.send("Hello")
generator.send("Python")
Received: Hello
Received: Python
send() is an advanced generator
feature. It becomes especially interesting
when building stateful pipelines and coroutine-
style systems.
close()
The close() method tells a generator to stop
execution.
def numbers():
try:
yield 1
yield 2
yield 3
finally:
print("Generator closed")
generator = numbers()
print(next(generator))
generator.close()
1
Generator closed
The finally block is useful for cleanup
when a generator is closed.
throw()
The throw() method allows an exception to
be injected into a generator at its current pause point.
def demo():
try:
yield "Waiting..."
except ValueError:
yield "ValueError received"
generator = demo()
print(next(generator))
print(generator.throw(ValueError))
Waiting...
ValueError received
This is an advanced feature and is less common in everyday beginner-level Python code.
Practical Generator Examples
Example 1 — Even Numbers
def even_numbers(limit):
for number in range(2, limit + 1, 2):
yield number
for number in even_numbers(10):
print(number)
Example 2 — Squares
def squares(limit):
for number in range(1, limit + 1):
yield number ** 2
for square in squares(5):
print(square)
1
4
9
16
25
Example 3 — Reading Lines
Files are naturally iterable, and generators can be used to transform or filter file data.
def valid_lines(lines):
for line in lines:
line = line.strip()
if line:
yield line
data = [
"Ali",
"",
"Sara",
"Ahmed"
]
for line in valid_lines(data):
print(line)
Example 4 — Data Pipeline
def numbers():
for number in range(1, 11):
yield number
def even_numbers(values):
for number in values:
if number % 2 == 0:
yield number
def squares(values):
for number in values:
yield number ** 2
pipeline = squares(
even_numbers(
numbers()
)
)
for value in pipeline:
print(value)
4
16
36
64
100
This demonstrates how generators can be chained together to create efficient data-processing pipelines.
Common Mistakes
1. Confusing yield with return
yield pauses a generator, while
return ends the generator.
2. Expecting a generator to store all values
Generators are designed to produce values on demand.
3. Trying to index a generator
A generator does not normally support direct
indexing such as generator[0].
4. Forgetting generators are consumed
Once values have been consumed, the generator normally cannot simply be restarted.
5. Using generators when random access is needed
If your application repeatedly needs values by index, a list or another suitable data structure may be a better choice.
Practice Exercises
Exercise 1 — Number Generator
Create a generator that produces numbers from 1 to 10.
Exercise 2 — Odd Numbers
Create a generator that produces odd numbers up to a specified limit.
Exercise 3 — Cubes
Create a generator that produces the cubes of numbers from 1 to 10.
Exercise 4 — Multiplication Table
Build a generator that produces the multiplication table of a given number.
Exercise 5 — Positive Numbers
Create a generator that receives a list and yields only positive values.
Exercise 6 — Generator Expression
Rewrite a list comprehension as a generator expression and observe the difference in behavior.
Mini Challenge — Sales Data Generator
Build a generator that processes sales records one at a time.
sales = [
{"product": "Laptop", "amount": 1200},
{"product": "Phone", "amount": 800},
{"product": "Tablet", "amount": 500},
{"product": "Monitor", "amount": 300},
{"product": "Keyboard", "amount": 100}
]
def high_value_sales(records, minimum):
for sale in records:
if sale["amount"] >= minimum:
yield sale
for sale in high_value_sales(sales, 500):
print(sale)
{'product': 'Laptop', 'amount': 1200}
{'product': 'Phone', 'amount': 800}
{'product': 'Tablet', 'amount': 500}
Your Tasks
- Create a generator that yields only sales above a specified amount.
- Create another generator that calculates a discount for each sale.
- Create a generator that yields only products with an amount greater than 700.
- Chain two generators together.
- Convert the final generator to a list when you actually need all results at once.
Imagine these records came from a large database or API containing millions of sales. A generator lets you process the records progressively instead of loading everything into memory at once.
Knowledge Check
Test your understanding of Python generators.
1. Which keyword creates a generator function?
2. What does yield do?
3. What does next() do with a generator?
4. What happens when a generator has no more values?
5. Why are generators memory efficient?
6. What is lazy evaluation?
7. What is a generator expression?
8. Which uses parentheses for a generator expression?
9. What does send() allow?
10. Are generators always better than lists?
Lesson Summary
- A generator produces values one at a time.
-
Generator functions use the
yieldkeyword. -
yieldpauses execution while preserving the generator's state. -
next()requests the next value. -
A generator raises
StopIterationwhen it is exhausted. - Generators are naturally lazy.
- Lazy evaluation can significantly reduce memory usage.
- Generator expressions provide a compact syntax for simple generators.
- Generators are useful for large files, datasets, APIs, streams, and data pipelines.
-
send(),close(), andthrow()provide advanced control over generators. - Generators are not always the right choice; lists are often better when repeated access, indexing, or reuse is required.
Excellent Work!
You now understand how Python generates values lazily and how generators can make data processing much more efficient.