Generators and yield
Generators and yield
A generator is an iterator you write as a function. yield pauses the function and hands a value to the caller. The next next (or the next step of a for) resumes just after that yield. The boring default is a generator function, not a custom iterator class. You get __iter__ and __next__ for free, and you do not manage an index.
Mental model
return ends a function. yield parks it. The object you get from calling a generator function is the generator — calling the function does not run the body yet. The body runs up to the first yield when something pulls a value.
yield from xs is “walk this iterable and yield each item.” It is the readable way to flatten two shifts into one stream.
A generator expression is a comprehension in parentheses: (t["id"] for t in tickets if t["open"]). It is lazy. It is also one-shot, like any generator.
send pushes a value into a generator at a yield expression. You will rarely need it at a desk. A function argument is clearer. The example below is enough to recognise it.
Worked examples
Case 1: yield is a window
Save as window_gen.py. Compare this to the TicketWindow class in the previous chapter. Same behaviour, no index.
# window_gen.py
def ticket_window(tickets):
for ticket in tickets:
yield ticket
def main():
window = ticket_window(["T-41", "T-42", "T-43"])
print(type(window).__name__)
print(next(window))
for ticket in window:
print(f"served {ticket}")
if __name__ == "__main__":
main()Run:
uv run python window_gen.pyOutput:
generator
T-41
served T-42
served T-43
Calling ticket_window(...) built the generator. next ran the body until the first yield. The for continued from there.
Case 2: yield from merges shifts
Save as merge_shifts.py. Morning and evening are two lists. The desk wants one stream.
# merge_shifts.py
def all_tickets(morning, evening):
yield from morning
yield from evening
def main():
morning = ["T-51", "T-52"]
evening = ["T-61"]
print(list(all_tickets(morning, evening)))
for ticket in all_tickets(morning, evening):
print(ticket)
if __name__ == "__main__":
main()Run:
uv run python merge_shifts.pyOutput:
['T-51', 'T-52', 'T-61']
T-51
T-52
T-61
Each call to all_tickets is a new generator, so list(...) and the for both see every ticket. yield from morning is not a copy of the list; it walks morning as the caller pulls.
Case 3: A generator expression
Save as open_ids.py. Only open tickets leave the rail. The expression does not build a second list of dicts.
# open_ids.py
def main():
tickets = [
{"id": "T-71", "open": True},
{"id": "T-72", "open": False},
{"id": "T-73", "open": True},
]
open_ids = (t["id"] for t in tickets if t["open"])
print(type(open_ids).__name__)
print(list(open_ids))
print(list(open_ids))
if __name__ == "__main__":
main()Run:
uv run python open_ids.pyOutput:
generator
['T-71', 'T-73']
[]
The second list is empty because the generator was spent. If you need the ids twice, use a list comprehension, or call a generator function twice.
Case 4: send, briefly
Save as pager.py. The first next (or send(None)) runs to the first yield and gets "ready". Later send values appear as the result of that yield. Sending None ends the loop.
# pager.py
def pager():
ticket = yield "ready"
while ticket is not None:
ticket = yield f"paged {ticket}"
def main():
station = pager()
print(next(station))
print(station.send("T-81"))
print(station.send("T-82"))
try:
station.send(None)
except StopIteration:
print("pager closed")
if __name__ == "__main__":
main()Run:
uv run python pager.pyOutput:
ready
paged T-81
paged T-82
pager closed
If you find yourself designing a protocol around send, stop. Pass the ticket as a function argument, or put tickets on a queue. send is in the language; it is not the desk default.
The trap
Save as class_vs_gen.py. The class stores an index and a copy of the list. The generator does not. Prefer the generator unless you need extra methods (peek, close_window) that a function cannot hold.
# class_vs_gen.py
class TicketWindow:
def __init__(self, tickets):
self._tickets = list(tickets)
self._i = 0
def __iter__(self):
return self
def __next__(self):
if self._i >= len(self._tickets):
raise StopIteration
ticket = self._tickets[self._i]
self._i += 1
return ticket
def ticket_window(tickets):
for ticket in tickets:
yield ticket
def main():
rail = ["T-91", "T-92"]
print("class", list(TicketWindow(rail)))
print("gen", list(ticket_window(rail)))
if __name__ == "__main__":
main()Run:
uv run python class_vs_gen.pyOutput:
class ['T-91', 'T-92']
gen ['T-91', 'T-92']
Same output. The generator is the one you keep. Write a class when the window has state a caller must poke between pulls. That is rarer than tutorials suggest.
The boring rule
- Write a generator function with
yieldinstead of an iterator class. yield fromflattens another iterable. Nestedforplusyieldis the long form of the same idea.- Generator expressions are lazy and one-shot. List comprehensions are eager and reusable.
- Do not build a
sendprotocol. Call a function. list(gen)is fine for a small rail. Do notlist()a stream you meant to keep lazy.
Try this
- In
window_gen.py,yielda formatted stringserved {ticket}instead of the raw id. - Add a third iterable
latetoall_ticketsandyield fromit last. - In
open_ids.py, replace the generator expression with a generator functiondef open_ids(tickets):thatyields. Call it twice. - Drop
sendfrompager.py. Makepager(tickets)a generator that yieldspaged {ticket}for each id in a list argument.