Create your own
Lesson illustration

Understanding Generator Functions and Yield

Hello! Welcome to the fourth lesson in our course on iteration in Python.

Introduction

In our previous lessons, we've built a solid foundation by differentiating iterables from iterators and, most recently, examining the role of the StopIteration exception as a control signal to terminate loops. We saw that implementing the iterator protocol manually requires creating a class with __iter__ and __next__ methods and carefully managing state to know when to raise StopIteration.

This lesson introduces a far more elegant and common way to create iterators in Python. Our learning outcome is to define a generator function using the yield keyword and explain how it differs from a regular function.

We will see that generators are a special kind of function that automatically produces an iterator, handling all the state management and the StopIteration signal for us. They are a cornerstone of efficient, readable Python code, especially for data processing and, as we'll see later, asynchronous programming.

To start, here is a quick visual analogy that captures the core difference we'll be exploring.

Caption: An animated GIF comparing a regular function to a generator. The function processes and delivers all items at once, like a full shopping cart. The generator produces and delivers items one by one as they are needed, like items on a conveyor belt.

What is a Generator Function?

At its core, a generator function looks like a regular Python function, but with one key difference: instead of using return to send back a value, it uses yield. The presence of the yield keyword is what transforms the function into a generator.

Let's see what this means in practice.

# A regular function that returns a list
def get_squares_list(n):
    squares = []
    for i in range(n):
        squares.append(i * i)
    return squares

# A generator function that yields values
def get_squares_generator(n):
    for i in range(n):
        yield i * i

Notice the only significant change is replacing the list-building logic and the final return with a single yield statement inside the loop.

The most important initial difference is what happens when you call them:

  • Calling get_squares_list(5) executes the function immediately and returns a complete list: [0, 1, 4, 9, 16].
  • Calling get_squares_generator(5) does not execute the function's code. Instead, it immediately returns a special generator object.
>>> squares_gen = get_squares_generator(5)
>>> print(squares_gen)
<generator object get_squares_generator at 0x...>

This generator object is an iterator. It "knows" how to produce the values by running the function's code on demand.

The following video provides a clear walkthrough of this transformation and the initial interaction with a generator object.

Python Tutorial: Generators - How to use them and the benefits you receive

This clip from Corey Schafer's tutorial demonstrates the simple conversion of a regular function to a generator and introduces the concept of the generator object.

Watch from 01:02 to 02:51. First, observe the direct replacement of return with yield. Then, pay close attention to how the generator object doesn't produce values until next() is called on it, retrieving one value at a time.

Key Differences Between Generators and Regular Functions

The distinction between returning a value and yielding a value leads to fundamental differences in execution, state management, and performance.

The article "What is a generator in Python?" provides a concise summary of these differences.

What is a generator in Python? How is it different from ...

This Medium article clearly outlines the primary distinctions between regular functions and generators.

Please read the section "Differences Between Generators and Regular Functions". This will give you a structured overview of the concepts we are about to explore in more detail: Return vs. Yield, State Preservation, and Memory Efficiency.

Let's break down these points.

1. Execution Flow and State Preservation (return vs. yield)

  • Regular Function (return): When a regular function is called, it runs from top to bottom, computes a result, returns it, and then it's done. Its execution frame is destroyed, and all local variables are discarded. If you call it again, it starts from scratch.

  • Generator Function (yield): When next() is called on a generator object for the first time, the function's code runs until it hits a yield statement. At that point, it does two things:

    1. It provides the yielded value to the caller.
    2. It pauses its execution and saves its entire state (including all local variables).

When next() is called again, the function resumes execution right where it left off, with all its variables intact. This "pause and resume" behavior is the defining characteristic of generators.

This diagram illustrates the process:

Caption: A diagram showing a generator function `gen_seq()`. Each call to `__next__()` causes the function to run until it hits `yield`, at which point it returns a value and pauses. The subsequent call resumes execution from that paused state.

When the generator function finishes (i.e., exits without hitting another yield), it automatically raises StopIteration for you, fulfilling the iterator protocol we discussed in the last lesson. This is why for loops work seamlessly with generators.

2. Memory Efficiency (Lazy Evaluation)

The most significant practical benefit of generators is their memory efficiency. Because they produce values one at a time—a concept known as lazy evaluation—they don't need to store the entire sequence in memory.

A regular function that builds and returns a large list can consume a vast amount of RAM. A generator, by contrast, only needs enough memory to hold its internal state and the single value it is currently yielding.

This makes generators ideal for working with:

  • Very large datasets (e.g., processing lines from a multi-gigabyte file).
  • Data streams.
  • Infinite sequences.

The following video segment provides a powerful, quantitative demonstration of this performance difference.

Python Tutorial: Generators - How to use them and the benefits you receive

In this part of his tutorial, Corey Schafer compares the memory usage and execution time of a function returning a list of one million items versus a generator yielding them.

Watch from 05:47 to 10:57. Focus on the stark contrast in memory consumption (mem_after) between the list-based approach and the generator-based approach. This is a direct consequence of lazy evaluation.

A Note on Generator Expressions

Just as Python has list comprehensions for creating lists concisely, it also has generator expressions for creating generators. The syntax is nearly identical, but you use parentheses () instead of square brackets [].

# List comprehension (builds a full list in memory)
list_comp = [i * i for i in range(1000)]

# Generator expression (creates a generator object)
gen_exp = (i * i for i in range(1000))

print(list_comp) # Prints a list of 1,000 numbers
print(gen_exp)   # Prints a generator object

Generator expressions are a compact and highly readable way to create simple generators on the fly.

Conclusion

In this lesson, we've defined generator functions and contrasted them with regular functions. The yield keyword is the key that unlocks a different mode of execution, offering powerful benefits.

Key Takeaways:

  • A function containing the yield keyword is a generator function.
  • Calling a generator function returns a generator object, which is a ready-to-use iterator.
  • Regular functions return a value and terminate, losing their state. Generators yield a value and pause, preserving their state for the next call.
  • This "lazy evaluation" makes generators extremely memory-efficient, as they produce values on demand rather than storing them all at once.
  • Generator expressions (item for item in iterable) provide a concise syntax for creating simple generators.

Next Up:
We've established that generators pause and resume. In the next lesson, we will trace the execution of a generator function in detail. By stepping through the code line-by-line, we'll solidify our understanding of how its state is suspended and resumed with each yield.

Can't find a good explanation? Sign up and we'll make it for you

Sign up