FastAPI x GenAI

Generators

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))  # genai
  1. iter(tags) gives us an iterator over the list. It is just a cleaner way to call tags.__iter__().

  2. Each next() hands out one element. Python remembers how far we have traversed.

  3. Call next() once more and we get a StopIteration error. 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 2

Generators: 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 2

Generator 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:

  1. Square brackets build a list right now. Round brackets build a generator that produces values only when asked.

  2. published_titles has 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.