Back to Blog
Python

Python yield vs return: How Generators Work

Understand how yield and return differ in Python, how to write generator functions, and when to choose each for lazy, memory-efficient iteration.

generatorslazy evaluationiteratorsyieldmemory efficiency
Illustration of a Python generator function with yield producing a lazy sequence of values, symbolizing memory-efficient iteration.

If you are trying to decide between yield and return in Python, the key distinction is whether the function should produce a single value or a lazy sequence of values. Any function that contains yield becomes a generator function: calling it returns a generator object rather than a normal result. This article explains how generators work, how yield differs from return, and when to use each.

What Does yield Do in Python?

A function that contains the yield keyword is a generator function. When called, it does not execute immediately. Instead, it returns a generator object that can be iterated. Each time the generator's __next__() method is invoked, the function runs until the next yield statement, pauses, and preserves its local state. On the next call, execution resumes from where it left off.

Consider a simple generator:

def count_up_to(n): i = 0 while i < n: yield i i += 1

Calling count_up_to(3) returns a generator object, not a list. You can iterate over it with a for loop or manually with next():

counter = count_up_to(3) print(next(counter)) # 0 print(next(counter)) # 1 print(next(counter)) # 2 # next(counter) would raise StopIteration

The key point is that the function's local variables (i in this case) are retained between calls. This statefulness is what makes generators powerful for streaming data.

How yield Differs from return

The most obvious difference is that a generator can contain multiple yield statements and produce a value each time one is reached. A return statement ends execution as soon as it is encountered. With yield, the function pauses but does not terminate; it can produce a sequence of values over time.

Another critical difference is that return in a generator function (without a value) stops the iteration and raises StopIteration. If you write return value inside a generator, that value is not yielded; it is attached to the StopIteration exception and is generally not accessible through normal iteration. This behavior is often a source of confusion.

Here is a comparison:

Featurereturnyield
ExecutionTerminates functionPauses function
Values producedOne (or none)Multiple over time
State preservedNoYes
Function call resultReturned value (or None)Generator object
Use caseCompute a single resultProduce a sequence lazily

Writing a Generator Function with yield

A generator function is defined like any other function but uses yield to produce values. Here is a practical example that reads lines from a large file without loading the entire file into memory:

def read_large_file(file_path): with open(file_path) as file: for line in file: yield line.strip()

This function yields one line at a time. When used in a loop, it processes the file incrementally, which is far more memory-efficient than reading all lines into a list.

You can also combine yield with other logic. For instance, this generator filters and transforms data:

def even_squares(numbers): for n in numbers: if n % 2 == 0: yield n * n

This generator takes an iterable and yields only the squares of even numbers. The caller controls how many values are consumed, which is useful for infinite or very large sequences.

Consuming Generators: next() and for Loops

The primary ways to consume a generator are the for loop and the built-in next() function. The for loop implicitly calls next() until StopIteration is raised. This is the most common pattern because it handles termination automatically.

for value in count_up_to(5): print(value)

If you need to pull values one at a time, next() gives you explicit control. This is useful when you want to pause consumption or when you need to handle the StopIteration exception manually.

gen = count_up_to(2) print(next(gen)) # 0 print(next(gen)) # 1 try: next(gen) except StopIteration: print("Generator exhausted")

Generators are single-use iterators. Once exhausted, they cannot be reused. If you need to iterate again, you must create a new generator object.

When to Use yield vs return

The choice between yield and return depends on whether you need a single result or a lazy sequence. Use return when a function computes one value and you want that value immediately. Use yield when you want to produce a series of values, especially if the sequence is large or potentially infinite.

Here are concrete decision criteria:

  • Use return when the function computes a single value, such as a helper that returns sum(numbers) or looks up a user name by ID.
  • Use yield when the function produces an iterable that can be consumed incrementally, such as reading a file, generating a Fibonacci sequence, or walking a directory tree.
  • Use yield when you want to avoid building a full list in memory. For example, Python's range() returns a range object rather than a list.
  • Use return when the caller expects a concrete collection like a list or tuple, and the data is small enough to fit in memory.

A generator can also be used to implement custom iterators without creating a separate class. This reduces boilerplate and keeps the code concise.

Memory and Performance Considerations

Generators are lazy: they produce values only when requested. This has a direct impact on memory usage. Instead of allocating a list with all elements, a generator holds only the current state and produces one value at a time. For large datasets, this can be the difference between running out of memory and processing data smoothly.

However, laziness has a performance tradeoff. Each yield incurs overhead compared to a simple list iteration. If the sequence is small and you need random access, a list is faster. If you are processing a large stream, the memory savings of a generator often outweigh the per-item overhead.

Another consideration is that generators are single-pass. If you need to iterate over the data multiple times, you must either recreate the generator or materialize it into a list. This tradeoff is important when designing data pipelines.

Common Pitfalls with yield and return

One common mistake is using return with a value inside a generator, expecting it to be yielded. For example:

def wrong_generator(): yield 1 return 2

When iterating, you get 1 and then StopIteration. The 2 is not yielded; it is stored in the exception's value attribute, which is rarely accessed. This behavior is often surprising and should be avoided unless you have a specific need for the return value in exception handling.

Another pitfall is accidentally returning a generator expression from a normal function when you intended to return a list. If you write return [i for i in range(10)], you get a list. If you write return (i for i in range(10)), you get a generator object. The latter is a compact way to create a generator, but it is not the same as defining a generator function.

Finally, be careful with recursion and generators. A recursive generator can be elegant, but it must yield from the recursive call, not return it. Use yield from to delegate to another generator or iterable:

def flatten(nested): for item in nested: if isinstance(item, list): yield from flatten(item) else: yield item

The yield from expression simplifies delegation and is a key tool for building generator hierarchies.

Advanced: yield from and Generator Delegation

yield from allows a generator to yield values from another iterable or generator. This is particularly useful when composing generators. For example, you can chain generators to build a pipeline:

def numbers(): yield 1 yield 2 def doubled(): for n in numbers(): yield n * 2

Using yield from, the same can be written more directly:

def doubled(): yield from (n * 2 for n in numbers())

yield from also handles the communication between the caller and a sub-generator, including sending values and exceptions. This is a more advanced topic, but it is the foundation for coroutine-like patterns in Python.

When you have a generator that needs to produce values from multiple sources, yield from reduces boilerplate and makes the flow explicit. It is a clear signal that one generator is delegating to another, which improves readability and maintainability.

Python yield vs return: Generator Functions Explained | RYUSLOG DEV