Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›Generators and the yield keyword

Advanced Python

Generators and the yield keyword

Generators are specialized functions that return an iterator, allowing you to produce a sequence of values lazily over time. Unlike standard functions that return a single result and terminate, generators maintain their internal state between successive calls to produce values on demand. This approach is essential for memory efficiency when working with large datasets or infinite streams that would otherwise overwhelm system resources.

The Anatomy of a Generator Function

A generator function is defined just like a standard function, but instead of using the 'return' keyword to provide a result, it uses 'yield'. When a generator function is called, it does not actually execute the code inside the block immediately. Instead, it returns a generator object, which acts as an iterator. When you request the next value from this object, the function resumes execution from where it last left off until it encounters another yield statement. This mechanism allows the function to pause and save its entire local state—including local variables and the execution pointer—automatically. Because it holds its state in suspension rather than destroying the stack frame, it enables a sophisticated way of producing data incrementally. Understanding that 'yield' acts as a temporary pause button is fundamental to mastering how Python manages iterative data flow without requiring complex state-management classes.

# A generator function to yield numbers in a range
def countdown(n):
    print(f"Starting countdown from {n}")
    while n > 0:
        yield n  # Execution pauses here and returns the current value
        n -= 1

gen = countdown(3)
print(next(gen))  # Resumes, prints 3
print(next(gen))  # Resumes, prints 2

Lazy Evaluation and Memory Efficiency

The core philosophy behind generators is lazy evaluation. In traditional approaches, generating a list of one million items requires allocating memory for every single element simultaneously before the first item can even be processed. This is highly inefficient if you only need to iterate over the items one at a time. Generators bypass this by calculating the next item only when it is explicitly requested by the caller. By yielding items individually, the generator maintains a constant memory footprint, regardless of how many millions of items the sequence eventually produces. This design is crucial for handling large files, network streams, or computationally expensive sequences where pre-calculating everything would cause a memory overflow. When you reason about generators, visualize them as a pipeline where data is pulled through only as needed, ensuring the application remains responsive and light even when working with massive input volumes.

import sys

# A list comprehension creates the whole object in memory
list_comp = [x * 2 for x in range(1000000)]
# A generator expression creates a stream on demand
gen_exp = (x * 2 for x in range(1000000))

print(f"List size: {sys.getsizeof(list_comp)} bytes")
print(f"Generator size: {sys.getsizeof(gen_exp)} bytes")

State Maintenance via Yield

One of the most powerful features of the yield keyword is its ability to preserve the local scope of a function across multiple iterations. When a function executes a return statement, the local variables are cleaned up and the scope is destroyed. Conversely, when a generator yields, the interpreter saves the local namespace and the current instruction pointer into the generator object. When 'next()' is called again, the interpreter restores that specific context, allowing local variables to persist between steps. This makes generators an excellent tool for implementing stateful logic, such as a state machine or a custom sequence generator, without needing to create a class with instance attributes to track progress. Because this state is isolated within each generator instance, you can safely create multiple independent iterators from the same generator function, each tracking its own unique position in the logic flow.

# A generator maintaining its own internal state
def fibonacci_sequence():
    a, b = 0, 1
    while True:
        yield a  # State of 'a' and 'b' is preserved between calls
        a, b = b, a + b

fibs = fibonacci_sequence()
print([next(fibs) for _ in range(5)]) # [0, 1, 1, 2, 3]

The Yield from Syntax

When dealing with nested data structures or when you want to delegate part of the generator's task to another generator, the 'yield from' syntax becomes indispensable. Instead of manually writing a loop to iterate over another iterable and yielding its items one by one, 'yield from' creates a transparent bridge. It automatically handles the iteration protocol, including passing values, catching exceptions, and managing the state between the caller and the sub-generator. This significantly simplifies code readability and reduces the likelihood of bugs in complex recursive structures or when flattening nested collections. By using 'yield from', you essentially delegate the yielding process to another source, which is especially useful when creating complex data processing chains where one stage needs to fully consume another stage before continuing. It is a syntactical shortcut that keeps your implementation clean while retaining full performance benefits.

def sub_generator():
    yield "inner-1"
    yield "inner-2"

def main_generator():
    yield "outer-1"
    yield from sub_generator() # Delegates directly to the sub-generator
    yield "outer-2"

for val in main_generator():
    print(val)

Communication with Generators

Generators are not just one-way data producers; they can also receive input from the caller using the 'send()' method. When a generator is paused at a yield statement, the 'yield' expression itself can be assigned to a variable. When the caller calls 'gen.send(value)', that value is injected into the generator, effectively becoming the result of the yield expression where the code was suspended. This allows for bi-directional communication, turning the generator into a coroutine capable of processing incoming data or adjusting its internal logic dynamically. This advanced feature is the foundation for asynchronous programming patterns and more complex data processing pipelines where the producer needs feedback from the consumer. While the initial setup requires starting the generator with a call to 'next()' or 'send(None)' to prime it to the first yield point, this bidirectional channel provides powerful control for sophisticated data processing tasks.

def accumulator():
    total = 0
    while True:
        # Yields total, but also expects to receive a value back
        added_val = yield total
        if added_val is not None:
            total += added_val

acc = accumulator()
next(acc) # Prime the generator
print(acc.send(10)) # Adds 10, returns 10
print(acc.send(5))  # Adds 5, returns 15

Key points

  • Generators are functions that return an iterator, allowing for lazy evaluation of sequences.
  • The yield keyword pauses function execution while preserving the local state and variables.
  • Using generators is significantly more memory-efficient than creating large lists in memory.
  • Generator objects are resumed using the next() function to obtain the subsequent value.
  • The yield from syntax allows a generator to delegate iteration to another generator or iterable.
  • Generators maintain their own execution state, eliminating the need for manual state-tracking variables.
  • The send() method enables two-way communication by allowing data to be injected back into a paused generator.
  • Lazy evaluation ensures that elements are only computed at the exact moment they are needed.

Common mistakes

  • Mistake: Expecting a generator to be subscriptable. Why it's wrong: Generators are iterators that generate values on-the-fly and do not store them in memory; they lack __getitem__. Fix: Convert the generator to a list using list() if random access is required.
  • Mistake: Trying to use 'return' with a value to pass data out of a generator. Why it's wrong: While 'return' is allowed in Python 3.3+, it raises a StopIteration exception containing the value rather than yielding it as part of the iteration. Fix: Always use 'yield' to emit values during iteration.
  • Mistake: Re-iterating over a generator that has already been exhausted. Why it's wrong: Generators are one-time-use objects; once the underlying execution reaches the end, they cannot be reset. Fix: Re-instantiate the generator function if the data needs to be traversed again.
  • Mistake: Confusing 'yield' with 'yield from'. Why it's wrong: 'yield' produces a single value, whereas 'yield from' delegates to another iterable or generator, yielding all its values. Fix: Use 'yield from' when delegating to sub-generators to properly handle return values and exceptions.
  • Mistake: Assuming a generator function starts executing as soon as it is called. Why it's wrong: Calling a generator function merely returns a generator object; code execution only begins upon the first call to next(). Fix: Remember that the function body is lazy and requires iteration to trigger execution.

Interview questions

What is a generator in Python, and how does it differ from a standard function?

A generator in Python is a special type of function that allows you to declare a function that behaves like an iterator. Unlike a standard function that uses the 'return' statement to return a single value and then terminates, a generator uses the 'yield' keyword to return a sequence of values over time, pausing its execution state in between. This makes generators incredibly efficient for handling large datasets because they do not store the entire result set in memory at once; they compute values lazily, one at a time, only when requested by the caller.

How does the yield keyword function during the execution of a Python generator?

When a Python generator function reaches a 'yield' statement, it pauses its execution and saves its entire internal state, including local variables and the instruction pointer. The value specified after 'yield' is sent back to the caller. When the next value is requested, the generator resumes execution immediately following the last 'yield' statement. This persistence of state is why generators are so powerful; they maintain their context across multiple iterations, allowing you to write clean, stateful code without needing to manually manage classes or instance attributes.

Compare the memory consumption of a list comprehension versus a generator expression when processing a million items.

Using a list comprehension, such as [x**2 for x in range(1000000)], forces Python to allocate enough memory to hold all one million integers in a list simultaneously. This can easily lead to a memory overflow error if the dataset is large enough. In contrast, a generator expression like (x**2 for x in range(1000000)) uses lazy evaluation. It calculates each square only when it is actually needed by a loop or consumer, meaning it occupies a near-constant, minimal amount of memory regardless of the total number of items in the sequence.

What is the purpose of the next() function and StopIteration exception in the context of generators?

In Python, generators implement the iterator protocol. When you call next(my_generator), Python executes the generator function until it hits a 'yield' statement and returns that value. If you call next() again, the generator resumes and yields the next value. Once the generator function finishes its execution or encounters a 'return' statement, it raises a 'StopIteration' exception. This exception signals to the iteration machinery, such as a 'for' loop, that no more items are available, effectively and gracefully terminating the iteration process without causing a crash.

Can you explain how the send() method works with generators, and how it differs from using next()?

While next() simply triggers the generator to produce the next yielded value, the send() method allows you to push a value back into the generator function. When you use 'x = yield', the generator pauses at the yield statement as usual, but the value passed into generator.send(value) becomes the result of the 'yield' expression inside the function. This transforms the generator from a data producer into a two-way communication channel, enabling advanced patterns like coroutines or cooperative multitasking where the generator can react to data provided by the outside caller.

How can you use the yield from syntax to delegate to sub-generators, and why is this useful?

The 'yield from' syntax, introduced in Python 3.3, allows a generator to delegate part of its operations to another generator or iterable. Instead of writing a manual nested loop like 'for item in sub_gen: yield item', you simply write 'yield from sub_gen'. This is powerful because it establishes a transparent connection between the caller and the sub-generator. It properly handles all communication, including 'send()' values and 'throw()' exceptions, making it essential for building complex, modular pipelines where multiple generators are composed together into a single, cohesive processing stream.

All Python interview questions →

Check yourself

1. What happens when a generator function is called in Python?

  • A.The function body executes until the first yield statement.
  • B.The function body executes until the first return statement.
  • C.A generator object is returned, and the function body is not yet executed.
  • D.The function executes completely and returns a list of values.
Show answer

C. A generator object is returned, and the function body is not yet executed.
Calling a generator function returns an iterator object without executing the function body. Option 1 is wrong because execution is lazy. Option 2 is wrong because return ends the generator. Option 4 is wrong because generators do not store all values.

2. Which of the following describes the memory behavior of a generator?

  • A.It loads all generated elements into RAM at once for fast access.
  • B.It stores only the current state, generating values on demand.
  • C.It uses a fixed amount of memory regardless of the number of elements yielded.
  • D.It requires a memory buffer to hold the yielded results.
Show answer

B. It stores only the current state, generating values on demand.
Generators are lazy, storing only the state required to produce the next item. Option 1 describes a list. Option 3 is wrong because local variables within the generator function can affect memory usage. Option 4 is incorrect as no output buffer is required.

3. What is the primary difference between 'yield' and 'yield from'?

  • A.yield is for lists, yield from is for dictionaries.
  • B.yield pauses the function, yield from terminates the generator immediately.
  • C.yield emits a single value, while yield from delegates iteration to a sub-iterable.
  • D.yield from is faster because it bypasses the function call stack.
Show answer

C. yield emits a single value, while yield from delegates iteration to a sub-iterable.
yield from is a syntax sugar used to delegate to another iterator, pulling all items from it. Option 1 is wrong as both work with iterables. Option 2 is wrong as yield from doesn't terminate the parent. Option 4 is incorrect regarding performance.

4. Given 'gen = (x**2 for x in range(3))', what is the result of list(gen) + list(gen)?

  • A.[0, 1, 4, 0, 1, 4]
  • B.[0, 1, 4]
  • C.[]
  • D.A TypeError is raised.
Show answer

B. [0, 1, 4]
Generators are exhausted after the first iteration. The first list() call consumes all values; the second call receives an empty iterator. Options 1 is wrong because the generator doesn't reset. Option 3 is wrong because the first list is created successfully.

5. How can you pass a value back into a running generator?

  • A.By using the send() method on the generator object.
  • B.By assigning a value to the yield keyword.
  • C.By calling next() with an argument.
  • D.It is impossible to pass data into a generator once it starts.
Show answer

A. By using the send() method on the generator object.
The send() method allows passing a value into a generator, which then becomes the result of the yield expression. Option 1 is wrong as yield is a statement, not a variable. Option 3 is wrong as next() only takes the generator object.

Take the full Python quiz →

← PreviousDecoratorsNext →Iterators and the Iterator Protocol

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app