Generators are functions that produce a sequence of values lazily — one at a time, on demand, without building the entire sequence in memory. Every time you call next() on a generator, it runs until it hits a yield, hands back the value, and freezes its entire stack frame. The next call picks up exactly where it left off, local variables and all.
This matters because real programs deal with data that doesn't fit in RAM: server logs, database result sets, streaming API responses, sensor feeds. A list comprehension that processes 10 million rows allocates 10 million objects. A generator expression processes one row at a time. Same logic, constant memory. Once you internalize this, you'll reach for generators whenever data flows through your program rather than sitting in it.
Master python generators
Take the Python Fundamentals course with hands-on lessons and challenges.
Each function yields one row at a time. The pipeline chains without intermediate lists, so a 4 GB CSV uses the same memory as a 4 KB one. This is the bread-and-butter use case for generators in data processing.
yield from flattens each page of results into a single stream. The caller doesn't know or care about pagination — it just iterates. The generator only fetches the next page when the current one is exhausted.
The generator expression is 40,000x smaller than the list for the same logical operation. Pass generator expressions directly to any(), all(), sum(), min(), max(), and ''.join() to avoid allocating throwaway lists.
send() pushes a value into the generator at the yield point. The generator processes it and yields the next result. You must call next() once first to advance to the initial yield — this is called 'priming'. This pattern predates asyncio and is still useful for stateful stream processors.
yield from delegates iteration to another generator or iterable. It's not just syntactic sugar for a for-loop — it correctly propagates send(), throw(), and return values through the chain, which matters when generators are used as coroutines.
Async generators combine await (for I/O) with yield (for lazy production). They're consumed with async for. This is the cleanest way to stream data from paginated APIs, message queues, or WebSocket connections without buffering everything in memory.
Calling a generator function and expecting it to run — `read_csv("data.csv")` returns a generator object, it doesn't read anything
A generator function call creates the iterator but executes zero lines of code. You must consume it: `for row in read_csv("data.csv")`, or `list(read_csv(...))`, or `next(gen)`. The body runs only when you pull values.
Trying to iterate over a generator twice — the second loop silently produces nothing
Generators are single-pass. Once exhausted, they're done. If you need multiple passes, either recreate the generator by calling the function again, or collect into a list first with `data = list(gen)`. Prefer recreating when memory is the concern.
Using `return value` in a generator and expecting the caller to receive it via iteration
A `return value` in a generator raises `StopIteration(value)`. Normal for-loops discard it. Only `yield from` captures the return value: `result = yield from sub_generator()`. Use `yield` for values the caller should see, `return` for a final result passed to a delegating generator.
Forgetting to prime a generator before calling `send()` — results in `TypeError: can't send non-None value to a just-started generator`
Always call `next(gen)` once before `send()`. This advances the generator to the first `yield` expression, where it can receive a value. A common pattern is a decorator that auto-primes: call `next()` in the wrapper and return the primed generator.
Generators are Python's mechanism for lazy, memory-efficient iteration. A `yield` in a function body turns it into a generator that produces values on demand while preserving its full stack frame between calls. Generator expressions provide the same laziness in a compact syntax. `yield from` enables clean composition of sub-generators and pipelines. `send()` and `close()` turn generators into stateful coroutines that accept input. Async generators extend the pattern to I/O-bound work. In practice, reach for generators whenever you process data that flows through your program — file streams, API pages, database cursors, transformation pipelines — rather than data that sits in memory all at once.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.