Home Lessons About
Home / Lessons / Generators
Lesson 51 Advanced Python Concepts

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.

50–60 minutes Intermediate Performance
Reading progress 0%
01

Learning Objectives

By the end of this lesson, you will understand how generators work and why they are extremely useful when processing data efficiently.

You will learn how to:
  • Understand generator functions.
  • Use the yield keyword.
  • Retrieve generator values with next().
  • Use generators with for loops.
  • Understand the difference between yield and return.
  • Understand generator state.
  • Understand lazy evaluation.
  • Build memory-efficient data processing pipelines.
  • Create generator expressions.
  • Understand advanced generator methods such as send(), close(), and throw().
02

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.

Simple generator
def numbers():

    yield 1
    yield 2
    yield 3


for number in numbers():
    print(number)
Output
1
2
3

Unlike a normal function, the generator does not execute completely when it is called. It produces each value when requested.

A generator is essentially a convenient way of creating an iterator.
03

The yield Keyword

The yield keyword is what makes a function a generator function.

Using yield
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.

Think of yield as a pause point

The generator gives a value to the caller, remembers where it stopped, and continues from that exact location later.

04

Generator Functions

A function containing yield becomes a generator function.

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.

A generator function and a normal function behave differently. A normal function executes when called, while a generator function returns a generator object that executes as values are requested.
05

Using next()

You can manually retrieve values from a generator using the built-in next() function.

Manual generator control
def numbers():

    yield 10
    yield 20
    yield 30


generator = numbers()

print(next(generator))
print(next(generator))
print(next(generator))
Output
10
20
30

After the final value has been produced, another next() call raises StopIteration.

06

Generators with for Loops

In normal Python programs, generators are often consumed with for loops.

Using a generator
def even_numbers():

    for number in range(2, 11, 2):
        yield number


for number in even_numbers():
    print(number)
Output
2
4
6
8
10

The for loop automatically requests values from the generator until the generator is exhausted.

07

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.
return
def get_number():

    return 10

    # Code after return
    # will not execute
yield
def get_numbers():

    yield 10
    yield 20
    yield 30
08

Generator State

One of the most powerful features of generators is that they remember their execution state.

Remembering position
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))
Output
Step 1
100
Step 2
200
Step 3
300

Notice that the generator does not restart from the beginning after every next() call.

09

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.

List approach
numbers = [number for number in range(1000000)]

The list stores all one million values.

Generator approach
numbers = (number for number in range(1000000))

The generator produces values when they are requested.

Generators can dramatically reduce memory usage when working with large or potentially unbounded sequences.
10

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.

Record generator
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.

Real-world applications

Generators are useful when processing large files, logs, database records, API responses, streams, and data pipelines.

11

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.

Generator expression
numbers = (number * 2 for number in range(5))

for number in numbers:
    print(number)
Output
0
2
4
6
8

The expression produces values lazily.

12

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.
Generators are not automatically better than lists. Choose the data structure according to the problem.
13

Lazy Evaluation

Generators use a concept called lazy evaluation.

Lazy evaluation means that a value is calculated only when it is needed.

Lazy execution
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))
Output
Generator created
Generating 1
1
Generating 2
2

Notice that the third value is never generated because it was never requested.

14

send()

Advanced generators can receive values from the caller using send().

Sending data to a generator
def receiver():

    while True:

        value = yield

        print("Received:", value)


generator = receiver()

next(generator)

generator.send("Hello")
generator.send("Python")
Output
Received: Hello
Received: Python
Important

send() is an advanced generator feature. It becomes especially interesting when building stateful pipelines and coroutine- style systems.

15

close()

The close() method tells a generator to stop execution.

Closing a generator
def numbers():

    try:

        yield 1
        yield 2
        yield 3

    finally:

        print("Generator closed")


generator = numbers()

print(next(generator))

generator.close()
Output
1
Generator closed

The finally block is useful for cleanup when a generator is closed.

16

throw()

The throw() method allows an exception to be injected into a generator at its current pause point.

Generator exception handling
def demo():

    try:

        yield "Waiting..."

    except ValueError:

        yield "ValueError received"


generator = demo()

print(next(generator))

print(generator.throw(ValueError))
Output
Waiting...
ValueError received

This is an advanced feature and is less common in everyday beginner-level Python code.

17

Practical Generator Examples

Example 1 — Even Numbers

Even number generator
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

Square generator
def squares(limit):

    for number in range(1, limit + 1):
        yield number ** 2


for square in squares(5):
    print(square)
Output
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

Generator 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)
Output
4
16
36
64
100

This demonstrates how generators can be chained together to create efficient data-processing pipelines.

18

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.

19

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.

20

Mini Challenge — Sales Data Generator

Build a generator that processes sales records one at a time.

Challenge starter
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)
Expected Output
{'product': 'Laptop', 'amount': 1200}
{'product': 'Phone', 'amount': 800}
{'product': 'Tablet', 'amount': 500}

Your Tasks

  1. Create a generator that yields only sales above a specified amount.
  2. Create another generator that calculates a discount for each sale.
  3. Create a generator that yields only products with an amount greater than 700.
  4. Chain two generators together.
  5. Convert the final generator to a list when you actually need all results at once.
Real-world thinking

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.

21

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?

22

Lesson Summary

  • A generator produces values one at a time.
  • Generator functions use the yield keyword.
  • yield pauses execution while preserving the generator's state.
  • next() requests the next value.
  • A generator raises StopIteration when 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(), and throw() 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.