Generators and yield

Updated

September 8, 2026

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.py

Output:

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.py

Output:

['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.py

Output:

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.py

Output:

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.py

Output:

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 yield instead of an iterator class.
  • yield from flattens another iterable. Nested for plus yield is the long form of the same idea.
  • Generator expressions are lazy and one-shot. List comprehensions are eager and reusable.
  • Do not build a send protocol. Call a function.
  • list(gen) is fine for a small rail. Do not list() a stream you meant to keep lazy.

Try this

  1. In window_gen.py, yield a formatted string served {ticket} instead of the raw id.
  2. Add a third iterable late to all_tickets and yield from it last.
  3. In open_ids.py, replace the generator expression with a generator function def open_ids(tickets): that yields. Call it twice.
  4. Drop send from pager.py. Make pager(tickets) a generator that yields paged {ticket} for each id in a list argument.