Imagine a waiter who brings your entire 7 course meal at once and dumps it on the table. That is a normal function returning a list. A generator is the waiter who brings one course, waits for you to finish, then brings the next. 🍜
"Why are we covering generators? That is Python stuff, completely unnecessary for APIs!" You are partially correct. But every batch of students eventually asks me the same question: why does our database session use a generator? So we are settling it now, once and for all. 😎
In the last lesson we taught our functions to declare their types. Now we teach them patience.
Why should you care?
Memory: A list of 1 million blog posts lives in RAM all at once. A generator produces one at a time, so memory stays flat.
Streaming: When an LLM answers you, tokens arrive one by one. FastAPI streams them to the browser using... generators.
Database sessions: FastAPI hands you a DB session with
yield. By the end of this post that line will stop looking like magic.
Iterators first
To understand generators, we first need iterators. An iterator gives you one element at a time. It must implement two methods, __iter__() and __next__(). Most Python data structures like lists, tuples, and strings are iterable.
tags = ["python", "fastapi", "genai"]tags_iter = iter(tags)print(next(tags_iter)) # pythonprint(next(tags_iter)) # fastapiprint(next(tags_iter)) # genaiiter(tags)gives us an iterator over the list. It is just a cleaner way to calltags.__iter__().Each
next()hands out one element. Python remembers how far we have traversed.Call
next()once more and we get aStopIterationerror. The kitchen is closed.
Why bother? Iterators save memory
Here is the fun part. A list of infinite blog titles would overflow your RAM. An iterator that produces them on demand will be totally fine:
class InfiniteBlogs: """Infinite iterator that keeps producing blog titles""" def __init__(self) -> None: self.num = 1 def __iter__(self): return self def __next__(self) -> str: title = f"Blog number {self.num}" self.num += 1 return titleblogs = iter(InfiniteBlogs())print(next(blogs)) # Blog number 1print(next(blogs)) # Blog number 2Generators: iterators for lazy people
We are developers and laziness is our nature. Writing __iter__ and __next__ every time gets old fast. Generators are an elegant shortcut: swap return for yield and Python builds the iterator for you.
from typing import Iteratordef infinite_blogs() -> Iterator[str]: num = 1 while True: yield f"Blog number {num}" num += 1blogs = infinite_blogs()print(next(blogs)) # Blog number 1print(next(blogs)) # Blog number 2Generator expressions
Comprehension syntax, but with round brackets:
class Blog: def __init__(self, title: str, tags: list[str], published: bool = False) -> None: self.title = title self.tags = tags self.published = publishedblogs = [Blog(f"Post {i}", tags=["python"]) for i in range(1000)]published_titles = (b.title for b in blogs if b.published)Let's review:
Square brackets build a list right now. Round brackets build a generator that produces values only when asked.
published_titleshas done zero work so far. It filters lazily as you loop over it.
One catch: a generator is single use. Loop over it once and it is empty. If we need the values again we create the generator again.
Back to that database session
Remember the question we started with? FastAPI dependencies use yield to hand you a session on demand: yield the session, pause, and after the request finishes, resume to close it. Setup and teardown in one function. The same trick powers StreamingResponse for sending LLM tokens as they arrive. We will meet both soon in upcoming section.
