Iterators & Generators
Generator send() and Coroutines
Sometimes generators need input while running - like a parser that needs the next chunk of data, or a state machine that receives commands. The send() method enables two-way communication, turning generators into simple coroutines.
Generator send() allows you to send values into a running generator, enabling two-way communication. This makes generators into simple coroutines.
Basic send() Usage
basic.py
Replay: real traced execution (multi-file project)
# Basic send()
def echo():
"""Generator that echoes received values"""
while True:
value = yield # receive value
print(f"Echo: {value}")
# Create and prime generator
gen = echo()
next(gen) # prime it (run until first yield)
# Send values
message = "hello"
gen.send(message)
gen.send(42)
gen.send([1, 2, 3])
# Basic send()
def echo():
"""Generator that echoes received values"""
while True:
value = yield # receive value
print(f"Echo: {value}")
# Create and prime generator
gen = echo()
next(gen) # prime it (run until first yield)
# Send values
message = "hi"
gen.send(message)
gen.send(42)
gen.send([1, 2, 3])
# Basic send()
def echo():
"""Generator that echoes received values"""
while True:
value = yield # receive value
print(f"Echo: {value}")
# Create and prime generator
gen = echo()
next(gen) # prime it (run until first yield)
# Send values
message = "ready"
gen.send(message)
gen.send(42)
gen.send([1, 2, 3])
gen ← ⟨generator object echo A⟩
11# Create and prime generator12gen→ ⟨generator object echo A⟩ = echo()13next(gen⟨generator object echo A⟩) # prime it (run until first yield)message ← hello, value ← hello
6 while True:7 value→ hello = yield # receive value8 print(f"Echo: {valuehello}")91011# Create and prime generator12gen = echo()13next(gen⟨generator object echo A⟩) # prime it (run until first yield)1415# Send values16message→ hello = "hello"17#@message="hi", "ready"18gen⟨generator object echo A⟩.send(messagehello)19gen.send(42)outputEcho: hellovalue ← 42
6 while True:7 value→ 42 = yield # receive value8 print(f"Echo: {value42}")91011# Create and prime generator12gen = echo()13next(gen) # prime it (run until first yield)1415# Send values16message = "hello"17#@message="hi", "ready"18gen⟨generator object echo A⟩.send(messagehello)19gen⟨generator object echo A⟩.send(42)20gen.send([1, 2, 3])outputEcho: 42value ← [1, 2, 3]
6 while True:7 value→ [1, 2, 3] = yield # receive value8 print(f"Echo: {value[1, 2, 3]}")91011# Create and prime generator12gen = echo()13next(gen) # prime it (run until first yield)1415# Send values16message = "hello"17#@message="hi", "ready"18gen.send(message)19gen⟨generator object echo A⟩.send(42)20gen⟨generator object echo A⟩.send([1, 2, 3])outputEcho: [1, 2, 3]gen.send([1, 2, 3])
19gen.send(42)20gen⟨generator object echo A⟩.send([1, 2, 3])
gen ← ⟨generator object echo A⟩
11# Create and prime generator12gen→ ⟨generator object echo A⟩ = echo()13next(gen⟨generator object echo A⟩) # prime it (run until first yield)message ← hi, value ← hi
6 while True:7 value→ hi = yield # receive value8 print(f"Echo: {valuehi}")91011# Create and prime generator12gen = echo()13next(gen⟨generator object echo A⟩) # prime it (run until first yield)1415# Send values16message→ hi = "hi"17gen⟨generator object echo A⟩.send(messagehi)18gen.send(42)outputEcho: hivalue ← 42
6 while True:7 value→ 42 = yield # receive value8 print(f"Echo: {value42}")91011# Create and prime generator12gen = echo()13next(gen) # prime it (run until first yield)1415# Send values16message = "hi"17gen⟨generator object echo A⟩.send(messagehi)18gen⟨generator object echo A⟩.send(42)19gen.send([1, 2, 3])outputEcho: 42value ← [1, 2, 3]
6 while True:7 value→ [1, 2, 3] = yield # receive value8 print(f"Echo: {value[1, 2, 3]}")91011# Create and prime generator12gen = echo()13next(gen) # prime it (run until first yield)1415# Send values16message = "hi"17gen.send(message)18gen⟨generator object echo A⟩.send(42)19gen⟨generator object echo A⟩.send([1, 2, 3])outputEcho: [1, 2, 3]gen.send([1, 2, 3])
18gen.send(42)19gen⟨generator object echo A⟩.send([1, 2, 3])
gen ← ⟨generator object echo A⟩
11# Create and prime generator12gen→ ⟨generator object echo A⟩ = echo()13next(gen⟨generator object echo A⟩) # prime it (run until first yield)message ← ready, value ← ready
6 while True:7 value→ ready = yield # receive value8 print(f"Echo: {valueready}")91011# Create and prime generator12gen = echo()13next(gen⟨generator object echo A⟩) # prime it (run until first yield)1415# Send values16message→ ready = "ready"17gen⟨generator object echo A⟩.send(messageready)18gen.send(42)outputEcho: readyvalue ← 42
6 while True:7 value→ 42 = yield # receive value8 print(f"Echo: {value42}")91011# Create and prime generator12gen = echo()13next(gen) # prime it (run until first yield)1415# Send values16message = "ready"17gen⟨generator object echo A⟩.send(messageready)18gen⟨generator object echo A⟩.send(42)19gen.send([1, 2, 3])outputEcho: 42value ← [1, 2, 3]
6 while True:7 value→ [1, 2, 3] = yield # receive value8 print(f"Echo: {value[1, 2, 3]}")91011# Create and prime generator12gen = echo()13next(gen) # prime it (run until first yield)1415# Send values16message = "ready"17gen.send(message)18gen⟨generator object echo A⟩.send(42)19gen⟨generator object echo A⟩.send([1, 2, 3])outputEcho: [1, 2, 3]gen.send([1, 2, 3])
18gen.send(42)19gen⟨generator object echo A⟩.send([1, 2, 3])
send() - a method that resumes a generator and passes a value to the yield expression
Priming Generators
priming.py
Replay: real traced execution (multi-file project)
# Priming a generator
def receiver():
print("Generator started")
while True:
value = yield
print(f"Received: {value}")
# Without priming (error)
gen = receiver()
try:
gen.send(10) # ERROR: can't send to a just-started generator
except TypeError as e:
print(f"Error: {e}")
# With priming
gen = receiver()
next(gen) # prime it - runs until first yield
gen.send(10)
gen.send(20)
gen ← ⟨generator object receiver A⟩
11# Without priming (error)12gen→ ⟨generator object receiver A⟩ = receiver()13try:try:
12gen = receiver()13try:14 gen⟨generator object receiver A⟩.send(10) # ERROR: can't send to a just-started generator15except TypeError as e:except TypeError as e:
14 gen.send(10) # ERROR: can't send to a just-started generator15except TypeError as e:16 print(f"Error: {ecan't send non-None value to a just-started generator}")outputError: can't send non-None value to a just-started generatorgen ← ⟨generator object receiver B⟩
18# With priming19gen→ ⟨generator object receiver B⟩ = receiver()20next(gen⟨generator object receiver B⟩) # prime it - runs until first yield21gen.send(10)def receiver():
4def receiver():5 print("Generator started")6 while True:outputGenerator startedvalue ← 10
6 while True:7 value→ 10 = yield8 print(f"Received: {value10}")91011# Without priming (error)12gen = receiver()13try:14 gen.send(10) # ERROR: can't send to a just-started generator15except TypeError as e:16 print(f"Error: {e}")1718# With priming19gen = receiver()20next(gen⟨generator object receiver B⟩) # prime it - runs until first yield21gen⟨generator object receiver B⟩.send(10)22gen.send(20)outputReceived: 10value ← 20
6 while True:7 value→ 20 = yield8 print(f"Received: {value20}")91011# Without priming (error)12gen = receiver()13try:14 gen.send(10) # ERROR: can't send to a just-started generator15except TypeError as e:16 print(f"Error: {e}")1718# With priming19gen = receiver()20next(gen) # prime it - runs until first yield21gen⟨generator object receiver B⟩.send(10)22gen⟨generator object receiver B⟩.send(20)outputReceived: 20gen.send(20)
21gen.send(10)22gen⟨generator object receiver B⟩.send(20)
priming - calling next() once to advance the generator to its first yield before using send()
Bidirectional Communication
bidirectional.py
Replay: real traced execution (multi-file project)
# Send and receive
def running_average():
"""Calculate running average of sent values"""
total = 0
count = 0
while True:
value = yield total / count if count > 0 else 0
total += value
count += 1
# Use bidirectional generator
gen = running_average()
print("Initial:", next(gen)) # prime and get initial value
print("After 10:", gen.send(10))
print("After 20:", gen.send(20))
print("After 30:", gen.send(30))
gen ← ⟨generator object running_average A⟩
14# Use bidirectional generator15gen→ ⟨generator object running_average A⟩ = running_average()16print("Initial:", next(gen⟨generator object running_average A⟩)) # prime and get initial valuetotal ← 0, count ← 0
4def running_average():5 """Calculate running average of sent values"""6 total→ 0 = 07 count→ 0 = 08 while True:while True:
pass 1 of 47count = 08while True:9 value = yield total0 / count0 if count > 0 else 010 total += valueAll 4 passes — pass 1 is the card above pass totalcount1 0 0 2 10 1 3 30 2 4 60 3 value ← 10, total ← 10, count ← 1
8 while True:9 value→ 10 = yield total0 / count0 if count > 0 else 010 total→ 10 += value1011 count→ 1 += 1121314# Use bidirectional generator15gen = running_average()16print("Initial:", next(gen⟨generator object running_average A⟩)) # prime and get initial value1718print("After 10:", gen⟨generator object running_average A⟩.send(10))19print("After 20:", gen.send(20))outputInitial: 0 Initial: 0value ← 20, total ← 30, count ← 2
8 while True:9 value→ 20 = yield total10 / count1 if count > 0 else 010 total→ 30 += value2011 count→ 2 += 1121314# Use bidirectional generator15gen = running_average()16print("Initial:", next(gen)) # prime and get initial value1718print("After 10:", gen⟨generator object running_average A⟩.send(10))19print("After 20:", gen⟨generator object running_average A⟩.send(20))20print("After 30:", gen.send(30))outputAfter 10: 10.0 After 10: 10.0value ← 30, total ← 60, count ← 3
8 while True:9 value→ 30 = yield total30 / count2 if count > 0 else 010 total→ 60 += value3011 count→ 3 += 1121314# Use bidirectional generator15gen = running_average()16print("Initial:", next(gen)) # prime and get initial value1718print("After 10:", gen.send(10))19print("After 20:", gen⟨generator object running_average A⟩.send(20))20print("After 30:", gen⟨generator object running_average A⟩.send(30))outputAfter 20: 15.0 After 20: 15.0print("After 30:", gen.send(30))
19print("After 20:", gen.send(20))20print("After 30:", gen⟨generator object running_average A⟩.send(30))outputAfter 30: 20.0
yield expression - `value = yield result` both sends a result out and receives the next sent value
State Machines
state_machine.py
Replay: real traced execution (multi-file project)
# Stateful coroutine
def traffic_light():
"""Traffic light state machine"""
state = "red"
while True:
command = yield state
if command == "next":
if state == "red":
state = "green"
elif state == "green":
state = "yellow"
elif state == "yellow":
state = "red"
elif command == "reset":
state = "red"
# Use state machine
light = traffic_light()
print("Initial:", next(light))
print("Next:", light.send("next"))
print("Next:", light.send("next"))
print("Next:", light.send("next"))
print("Reset:", light.send("reset"))
light ← ⟨generator object traffic_light A⟩
20# Use state machine21light→ ⟨generator object traffic_light A⟩ = traffic_light()22print("Initial:", next(light⟨generator object traffic_light A⟩))state ← red
4def traffic_light():5 """Traffic light state machine"""6 state→ red = "red"7 while True:while True:
pass 1 of 56state = "red"7while True:8 command = yield statered9 if command == "next":All 5 passes — pass 1 is the card above pass state1 red 2 green 3 yellow 4 red 5 red command ← next
7 while True:8 command→ next = yield statered9 if command == "next":10 if state == "red":11 state = "green"12 elif state == "green":13 state = "yellow"14 elif state == "yellow":15 state = "red"16 elif command == "reset":17 state = "red"181920# Use state machine21light = traffic_light()22print("Initial:", next(light⟨generator object traffic_light A⟩))2324print("Next:", light⟨generator object traffic_light A⟩.send("next"))25print("Next:", light.send("next"))outputInitial: red Initial: redif command == "next":
pass 1 of 38command = yield state9if commandnext == "next":10 if state == "red":11 state = "green"All 3 passes — pass 1 is the card above pass state1 red → green 2 green → yellow 3 yellow → red state ← green
9if command == "next":10 if statered == "red":11 state→ green = "green"12 elif state == "green":command ← next
7 while True:8 command→ next = yield stategreen9 if command == "next":10 if state == "red":11 state = "green"12 elif state == "green":13 state = "yellow"14 elif state == "yellow":15 state = "red"16 elif command == "reset":17 state = "red"181920# Use state machine21light = traffic_light()22print("Initial:", next(light))2324print("Next:", light⟨generator object traffic_light A⟩.send("next"))25print("Next:", light⟨generator object traffic_light A⟩.send("next"))26print("Next:", light.send("next"))outputNext: green Next: greenstate ← yellow
11 state = "green"12elif stategreen == "green":13 state→ yellow = "yellow"14elif state == "yellow":command ← next
7 while True:8 command→ next = yield stateyellow9 if command == "next":10 if state == "red":11 state = "green"12 elif state == "green":13 state = "yellow"14 elif state == "yellow":15 state = "red"16 elif command == "reset":17 state = "red"181920# Use state machine21light = traffic_light()22print("Initial:", next(light))2324print("Next:", light.send("next"))25print("Next:", light⟨generator object traffic_light A⟩.send("next"))26print("Next:", light⟨generator object traffic_light A⟩.send("next"))27print("Reset:", light.send("reset"))outputNext: yellow Next: yellowstate ← red
13 state = "yellow"14 elif stateyellow == "yellow":15 state→ red = "red"16elif command == "reset":command ← reset
7 while True:8 command→ reset = yield statered9 if command == "next":10 if state == "red":11 state = "green"12 elif state == "green":13 state = "yellow"14 elif state == "yellow":15 state = "red"16 elif command == "reset":17 state = "red"181920# Use state machine21light = traffic_light()22print("Initial:", next(light))2324print("Next:", light.send("next"))25print("Next:", light.send("next"))26print("Next:", light⟨generator object traffic_light A⟩.send("next"))27print("Reset:", light⟨generator object traffic_light A⟩.send("reset"))outputNext: red Next: redstate ← red
15 state = "red"16elif commandreset == "reset":17 state→ red = "red"print("Reset:", light.send("reset"))
26print("Next:", light.send("next"))27print("Reset:", light⟨generator object traffic_light A⟩.send("reset"))outputReset: red
close() and throw()
close_throw.py
Replay: real traced execution (multi-file project)
# Closing and throwing
def logger():
"""Generator that logs messages"""
try:
while True:
msg = yield
print(f"[LOG] {msg}")
except GeneratorExit:
print("[LOG] Shutting down")
# Use close()
gen = logger()
next(gen)
gen.send("Starting")
gen.send("Processing")
gen.close() # triggers GeneratorExit
print("Generator closed")
# Use throw()
gen2 = logger()
next(gen2)
gen2.send("Message 1")
try:
gen2.throw(ValueError, "Simulated error")
except ValueError:
print("Caught exception from generator")
gen ← ⟨generator object logger A⟩
14# Use close()15gen→ ⟨generator object logger A⟩ = logger()16next(gen⟨generator object logger A⟩)17gen.send("Starting")msg ← Starting
7 while True:8 msg→ Starting = yield9 print(f"[LOG] {msgStarting}")10 except GeneratorExit:11 print("[LOG] Shutting down")121314# Use close()15gen = logger()16next(gen⟨generator object logger A⟩)17gen⟨generator object logger A⟩.send("Starting")18gen.send("Processing")output[LOG] Startingmsg ← Processing
7 while True:8 msg→ Processing = yield9 print(f"[LOG] {msgProcessing}")10 except GeneratorExit:11 print("[LOG] Shutting down")121314# Use close()15gen = logger()16next(gen)17gen⟨generator object logger A⟩.send("Starting")18gen⟨generator object logger A⟩.send("Processing")19gen.close() # triggers GeneratorExitoutput[LOG] Processinggen.send("Processing")
17gen.send("Starting")18gen⟨generator object logger A⟩.send("Processing")19gen⟨generator object logger A⟩.close() # triggers GeneratorExit20print("Generator closed")except GeneratorExit:
9 print(f"[LOG] {msg}")10except GeneratorExit:11 print("[LOG] Shutting down")output[LOG] Shutting down [LOG] Shutting downgen2 ← ⟨generator object logger B⟩
18gen.send("Processing")19gen⟨generator object logger A⟩.close() # triggers GeneratorExit20print("Generator closed")2122# Use throw()23gen2→ ⟨generator object logger B⟩ = logger()24next(gen2⟨generator object logger B⟩)25gen2.send("Message 1")outputGenerator closedmsg ← Message 1
7 while True:8 msg→ Message 1 = yield9 print(f"[LOG] {msgMessage 1}")10 except GeneratorExit:11 print("[LOG] Shutting down")121314# Use close()15gen = logger()16next(gen)17gen.send("Starting")18gen.send("Processing")19gen.close() # triggers GeneratorExit20print("Generator closed")2122# Use throw()23gen2 = logger()24next(gen2⟨generator object logger B⟩)25gen2⟨generator object logger B⟩.send("Message 1")26try:output[LOG] Message 1gen2.send("Message 1")
24next(gen2)25gen2⟨generator object logger B⟩.send("Message 1")26try:try:
25gen2.send("Message 1")26try:27 gen2⟨generator object logger B⟩.throw(ValueError<class 'ValueError'>, "Simulated error")28except ValueError:except ValueError:
27 gen2.throw(ValueError, "Simulated error")28except ValueError:29 print("Caught exception from generator")outputCaught exception from generator
close() and throw() - methods to gracefully shut down a generator or inject exceptions
How It Works
- Call
next(gen)to start the generator (prime it) - Generator runs until
yield - Call
gen.send(value)to resume and pass a value yieldexpression evaluates to the sent value- Generator continues until next
yield
Use Cases
- Coroutines: functions that can pause and receive input
- Pipelines: processing data with feedback
- State machines: receive commands, send responses
Exercise: practical.py
Build a running average calculator using generator send()