37.6 Sending Values into a Generator with send()
While generators are most commonly known for producing sequences of values using yield, they also support a powerful two-way communication mechanism through the .send() method. This method transforms a generator from a simple producer of data into a coroutine—a more collaborative function that can both receive and emit values, maintaining state between interactions.
The send() Method and the yield Expression
The key to understanding .send() lies in recognizing that yield is not just a statement; it’s an expression. When a generator is paused at a yield statement, the execution can be resumed in two ways:
- With
next(): Theyieldexpression evaluates toNone. - With
.send(value): Theyieldexpression evaluates to thevalueprovided.
This means the line x = yield y does two things, but at different times. When execution first reaches this line, it yields the value y to the caller and pauses. When the generator is resumed with .send(some_value), it receives some_value, assigns it to x, and then continues execution until the next yield or the function ends.
def coroutine():
print("Coroutine started, awaiting initial value...")
# Execution pauses here after yielding None initially.
# The value sent via .send() will be assigned to 'received'
received = yield # This first yield is for receiving only; it yields None.
print(f"Received: {received}")
while True:
# This yield both sends a value and waits to receive the next one.
response = yield f"Echo: {received}"
print(f"Received: {response}")
received = response
# Initialize the generator. Must advance it to the first yield.
gen = coroutine()
next(gen) # Output: Coroutine started, awaiting initial value...
# Now we can start sending values.
result = gen.send("Hello")
print(result) # Output: Echo: Hello
result = gen.send("World")
print(result) # Output: Echo: World
The Initial next() Call (Priming the Generator)
A critical and often overlooked step is the initial call to next(). A generator must always be advanced to its first yield expression before you can call .send(). This is because upon creation, the generator is at the very beginning of its function body, not yet at a point where it can receive a value. Attempting to .send() a value immediately results in a TypeError.
def my_generator():
value = yield # This is the first yield
print(f"Value received: {value}")
gen = my_generator()
# gen.send("test") # TypeError: can't send non-None value to a just-started generator
# The correct way:
next(gen) # "Primes" the generator, advancing it to the first `yield`.
gen.send("Success!") # Output: Value received: Success!
This priming step is so crucial that a common best practice is to decorate coroutines meant for use with .send() with a decorator that handles this initial next() call automatically.
Distinguishing Yield Behavior: None vs. Value
It’s important to carefully design your yield expressions based on whether their primary purpose is to emit a value or to receive one.
value = yield: Primarily for receiving. The yielded value isNone. Use this when the generator is meant to pause and wait for instruction or data from the caller.value = yield emitted_value: For two-way communication. The generator emitsemitted_valueand immediately pauses, ready to receive a value that will be assigned tovalueupon resumption.yield emitted_value: Primarily for emitting. The generator sends outemitted_valueand pauses. If resumed with.send(), the sent value is lost unless assigned. This is the standard behavior for producer-style generators.
Handling Termination and the StopIteration Exception
When a generator function returns (either by flowing off the end or via a return statement), it raises a StopIteration exception. This is true whether the final resumption was triggered by next() or .send(). The value attribute of the StopIteration exception will contain the return value.
def accumulator():
total = 0
try:
while True:
# Receive a value and add it to the total
new_value = yield
total += new_value
# Implicitly yield None here before looping again
except GeneratorExit:
# This is called if the generator is closed with .close()
print(f"Generator closing, final total was: {total}")
return total # This return value is attached to the GeneratorExit exception
gen = accumulator()
next(gen) # Prime it
gen.send(10)
gen.send(5)
gen.send(3)
try:
gen.send(None) # Sending None continues the loop
except StopIteration as e:
print(f"The generator finished. Returned value: {e.value}") # Output: The generator finished. Returned value: 18
Common Pitfalls and Best Practices
- Forgetting to Prime: Always call
next()once on a new generator before the first.send(). This is the most common mistake. - Closing Generators: If a generator with an infinite loop (like our echo example) is no longer needed, you must call
.close()on it to prevent resource leaks. This throws aGeneratorExitexception into the generator at the point of theyield, allowing it to perform any cleanup (e.g., closing files, network connections) inside atry...exceptorfinallyblock. - Use as Coroutines: The primary use case for
.send()is implementing coroutines for cooperative multitasking, managing stateful context (like a parser or a network protocol handler), or creating complex data processing pipelines where feedback is required. For simple iteration, useforloops andnext(). - Alternative to send() for Initialization: Often, the first value “sent” into a generator is actually configuration. A cleaner pattern is to pass this configuration as a regular function argument and use
yieldpurely for two-way communication after initialization.