Iterators and generators

A generator produces values one at a time and pauses between them. Trace a function with yield being consumed by next() and a for loop, and see that the code after a yield runs only when the next value is requested.

9 min Free

A list holds all its values in memory at once. An iterator is different: it is an object that hands out one value each time you call next() on it, and raises StopIteration when it has nothing left. A for loop is just next() in disguise: it calls iter() on the thing you loop over, then next() again and again until StopIteration tells it to stop.

A generator is the easiest way to write an iterator. You write a normal function but use yield instead of return. Each yield hands out one value and pauses the function right there, keeping its local variables. The next next() resumes from that exact spot. The function never runs ahead of the consumer and never builds a list.

The program below is a toy agent that yields three events: a thought, a tool result and an answer. Press Run and watch the [agent] lines. They are printed from inside the generator, and they appear only when the caller asks for the next event.

Iterators and generatorsReady
# A generator hands out agent events one at a time, only when asked.
def run_agent(question):
    print("  [agent] thinking about:", question)
    yield {"author": "agent", "text": "Let me check the weather tool"}
    print("  [agent] calling the tool")
    yield {"author": "tool", "temp_c": 31}
    print("  [agent] writing the answer")
    yield {"author": "agent", "text": "It is 31 C in Chennai"}
    print("  [agent] finished")


stream = run_agent("Weather in Chennai?")
print("type:", type(stream).__name__)

first = next(stream)
print("first:", first["text"])

for event in stream:
    print("event from", event["author"])

leftover = next(stream, None)
print("after the loop:", leftover)

Press Play to run the program line by line, or step with ← →.

00/16

Variables

Nothing defined yet.

Output

Nothing printed yet.

What to notice

  1. Calling a generator function runs nothing. run_agent(...) returns a generator object immediately. No [agent] thinking line appears until the first next().
  2. yield pauses; next resumes. The print on line 5 runs on the second next(), not the first. The function picks up after the yield, with question still in scope.
  3. for calls next for you. The loop continued from where the manual next() left off. Mixing them is unusual in real code, but it shows that both talk to the same paused function.
  4. The end is an exception you never see. When the body finishes, the generator raises StopIteration. The for loop catches it and stops. next(stream, None) returns the default instead of raising.
  5. Generators are lazy. Only one event exists at a time. That is why they are the tool for streams, large files and anything you do not want to hold in memory all at once.
  6. A generator expression is the one-line form. sum(x * y for x, y in zip(a, b)) computes a dot product without ever creating a list.

Why this matters for AI work

ADK’s runner is built on this idea. runner.run_async(...) does not return a finished answer; it returns an asynchronous stream of Event objects, and you consume it with async for event in .... Each event (a model call, a tool result, a state change, the final reply) is yielded as it happens, so your UI can show progress instead of waiting. Read Events with this picture in mind: the async version adds await, but the pause-and-resume behaviour is the one you just traced.

Check your understanding

1. What does calling a generator function do?
2. A generator has yielded its last value and its body has finished. What does the next call to next() do?
3. You need to process a log file of 10 GB line by line. Why is a generator a good fit?