Skip to Content
Course content

172: yield from and Delegating Generators

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first encounter yield from, it's easy to assume it's just some syntactic sugar—a lazier way of writing a for loop to yield items from another iterable. I see this all the time. Learners think, "Why bother learning a new keyword when I can just loop through a list and yield each element?"

Thinking yield from is just a for-loop shortcut

Let's look at why that mindset is limiting. If you use a for loop to delegate to another generator, you're essentially acting as a middleman who manually passes packages from one person to another. It works for simple data retrieval, but it breaks the moment you need to send information back into the generator.

def sub_generator():
    # We want to receive a configuration value from the caller
    config = yield "Ready for config"
    yield f"Processing with {config}"

def wrapper_loop():
    # The "shorthand" approach
    for item in sub_generator():
        yield item

gen = wrapper_loop()
print(next(gen))  # Output: Ready for config
try:
    gen.send("High-Priority") 
except StopIteration:
    pass
# Wait... where did "Processing with High-Priority" go?

In the example above, the wrapper_loop is just iterating. When you call gen.send("High-Priority"), that value is sent to the wrapper_loop, not the sub_generator. The wrapper_loop doesn't know what to do with a sent value, so it just ignores it or crashes. The communication chain is broken.

Building a transparent pipe for bidirectional communication

This is where yield from changes the game. It doesn't just iterate; it establishes a transparent bidirectional pipe between the caller and the sub-generator. Anything you send() or throw() into the main generator is passed directly through to the delegated one.

def sub_generator():
    config = yield "Ready for config"
    yield f"Processing with {config}"

def wrapper_delegator():
    # This creates a direct link to sub_generator
    yield from sub_generator()

gen = wrapper_delegator()
print(next(gen))  # Output: Ready for config
print(gen.send("High-Priority"))  # Output: Processing with High-Priority

I like to think of yield from as "opening a portal." The wrapper_delegator effectively steps aside and lets the caller talk directly to the sub_generator until it's finished. This is critical for complex asynchronous patterns or state machines where the inner generator needs to react to external inputs.

Capturing the final result of a delegated generator

There is one more "superpower" that a for loop completely lacks: the ability to capture the return value of the sub-generator. In Python, when a generator returns a value (using the return statement), that value is bundled into the StopIteration exception.

A for loop simply ignores this return value. But yield from captures it and allows you to assign it to a variable.

def gather_data():
    yield "Chunk 1"
    yield "Chunk 2"
    return "All chunks processed!"

def master_process():
    # yield from returns the value the sub-generator returned
    result = yield from gather_data()
    print(f"Sub-generator said: {result}")
    yield "Master process finished"

for val in master_process():
    print(val)

# Output:
# Chunk 1
# Chunk 2
# Sub-generator said: All chunks processed!
# Master process finished

This allows you to build hierarchical generators where the "parent" can make decisions based on the final outcome of a "child" process. It's a clean, elegant way to manage nested logic without having to manually catch StopIteration exceptions and dig through the value attribute.




📋 Practical Task

Exercise: Building a Nested Command Processor with Return Values

You are building a system that processes a sequence of commands. Some commands are "simple" and can be handled by the main generator, while others are "complex" and must be delegated to a specialized handler generator.

Requirements:

  • Create a generator called complex_handler(). It should yield the string "Processing complex task..." and then return the integer 100 (representing a success score).
  • Create a main generator called command_processor().
    • It should first yield the string "Starting processor".
    • It should then use yield from to delegate to complex_handler().
    • It must capture the return value from complex_handler().
    • Finally, it should yield a string that says "Complex task completed with score: [score]", using the captured return value.
  • Write the code to iterate through command_processor() and print every yielded value.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.