Practical guide · Python

Python yield Explained: Generators, Memory Efficiency, and Best Practices

Learn how yield turns a function into a lazy iterator, when generators help, and how to avoid common consumption and resource-handling mistakes.

Python's yield keyword can look like a variation of return, but it changes how a function runs. A function that yields values produces a generator: an iterator that can pause between values and resume its saved state when the next value is requested. This makes generators useful for sequential work with data that is large, streamed, or computed on demand.

What does yield do?

return gives a function's result back to its caller and ends that function call. yield produces one value and suspends the function. Its local variables and execution position are retained, and iteration can resume from that point.

A function containing yield is a generator function. Calling it creates a generator object; the function body starts running only when the generator is advanced.

return versus yield

Use a list when you want to build and return a small, finite collection all at once:

def numbers_with_return():
    return [1, 2, 3]

print(numbers_with_return())
# [1, 2, 3]

Use yield when values should be produced one at a time:

def numbers_with_yield():
    yield 1
    yield 2
    yield 3

for number in numbers_with_yield():
    print(number)

The loop requests each next value, so the caller can process it before asking for another. A generator is not a list, though: it supports iteration rather than general-purpose indexing and repeated traversal.

How a generator pauses and resumes

Each call to next() runs a generator until it reaches the next yield, returns that value, and pauses again. When there are no more yielded values, iteration ends with StopIteration; a for loop handles that signal automatically.

def steps():
    print("Start")
    yield 1
    print("Middle")
    yield 2
    print("End")
    yield 3

generator = steps()
print(next(generator))
print(next(generator))
print(next(generator))

Creating generator does not print anything. The first next() starts the body, prints “Start,” and returns 1. Later requests continue after the previous yield. This pause-and-resume behavior is why a generator can preserve state without constructing all its future values in advance.

Why generators can be memory efficient

A list comprehension constructs and retains every result:

numbers = [number for number in range(1_000_000)]

A generator expression computes values as they are requested:

numbers = (number for number in range(1_000_000))

for number in numbers:
    process(number)

Similarly, a generator function can describe a sequence without keeping a full result list:

def generate_numbers(limit):
    for number in range(limit):
        yield number

This can lower peak memory when each value is consumed and then discarded. It does not guarantee low memory in every program: values retained by the caller, internal buffers, or converting the generator to a list can still use substantial memory. Generators also add little value for tiny fixed collections that are simpler as lists.

Read a large file one line at a time

readlines() loads all lines into a list. A file object is already iterable, so a loop processes lines without reading the whole file into memory:

with open("app.log", encoding="utf-8") as log_file:
    for line in log_file:
        process(line)

You can also write a generator that transforms or filters each line as it is read:

def error_lines(path):
    with open(path, encoding="utf-8") as log_file:
        for line in log_file:
            if "ERROR" in line:
                yield line

for line in error_lines("app.log"):
    process(line)

The file stays open while the generator is being iterated and closes when iteration completes. If a consumer may stop early, close the generator explicitly so the file is released promptly:

from contextlib import closing

with closing(error_lines("app.log")) as lines:
    for line in lines:
        process(line)
        if should_stop(line):
            break

Generators with loops and streams

Generators work naturally with loops that calculate each value on demand:

def even_numbers(limit):
    for number in range(limit):
        if number % 2 == 0:
            yield number

for number in even_numbers(10):
    print(number)
# 0, 2, 4, 6, 8

A generator can also describe an unbounded sequence. Consume it with a stopping condition; an infinite generator will not finish if passed directly to operations that wait for all values, such as list().

def counter():
    number = 1
    while True:
        yield number
        number += 1

for number in counter():
    if number > 3:
        break
    print(number)

Common uses

Common mistakes and best practices

Rule of thumb: use yield when a caller can consume a sequence incrementally. Use a list when the complete collection is small and needs random access or repeated traversal. Measure memory and runtime for your actual workload rather than assuming a generator is always faster.