Generators & Iterators
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
Where the first post defined the terms, this one answers the question every engineer eventually asks: why bother? The honest answer is that generators change the relationship between the size of your data and the size of your memory footprint. That single property unlocks streaming, infinite sequences, pipelines, and early exit.
The framing to carry through the post is decoupling: how *much* you process should not dictate how much you *hold*. Generators let you process a terabyte while holding a kilobyte.
The headline win is constant memory. A list's memory grows linearly with the number of elements — a million-row list holds a million objects simultaneously. If your access pattern is "touch each element once and move on," that storage is pure waste.
A generator collapses that to O(1): it materializes one element, you use it, it's discarded, and the next is produced. For aggregations, filters, and transforms — the bulk of real data work — you never actually need all the elements present at once, which is exactly the situation generators are built for.
This code makes the memory claim tangible. Both snippets compute the same total character count over a file. The list-comprehension version pulls every line into RAM before summing. The generator-expression version — identical except for the bracket style — streams one line at a time.
For a small file the difference is invisible. For a multi-gigabyte log the list version crashes with a MemoryError while the generator version sails through using a few kilobytes. The lesson is that the syntactic difference is tiny but the runtime consequence is enormous, which is why knowing when to use which matters.
Processing data larger than RAM is the headline use case people remember. A file object, a database cursor, or a network stream can all represent more data than fits in memory, and generators let you walk them without ever holding the whole thing.
The canonical example is the multi-gigabyte CSV: loading it fully is impossible on a normal machine, but streaming it row by row is trivial and fast. This is why data-engineering and log-processing code leans so heavily on generators — the inputs routinely dwarf available memory.
Infinite sequences are the most striking demonstration of laziness. naturals() contains while True, which would loop forever if it ran eagerly — but it never runs ahead of demand. Each next() advances exactly one step and then pauses.
This lets you model genuinely unbounded streams: all natural numbers, an endless stream of random samples, a polling loop that never stops. The consumer decides how far to go. Pair this with a bounded consumer like itertools.islice and you get "the first N of an infinite stream" cleanly — a pattern impossible with eager lists.
The pipeline diagram previews the composition story. Each stage — read, filter, parse, aggregate — is itself a generator that takes the previous stage's output as input. Data doesn't pile up between stages; it flows through one item at a time.
This is the architectural payoff of generators. You write each transformation as a small, readable, independent step, yet the whole thing executes as a single streaming pass with no intermediate storage. You get the clarity of separate stages and the efficiency of fused execution simultaneously, which is rare.
Composition deserves its own slide because it's where generators move from "memory trick" to "design pattern." Because the output of one generator is a valid input to the next, you can build long chains: read lines, strip them, filter, parse, batch, write.
Nothing executes until the final consumer pulls a value, and then exactly one item threads through the entire chain before the next begins. Contrast this with the list approach, where each stage builds a full intermediate list — more memory and more passes. The generator chain is a single lazy pass that reads top to bottom.
Short-circuiting is an underrated benefit. Because generators only produce values on demand, stopping early genuinely avoids work — not just ignores it. Searching a huge file for the first ERROR line reads only up to that line and then stops; the remainder is never touched.
With a pre-built list, all the filtering happens up front regardless of how soon you'd have found your answer. Pairing generators with next(), any(), all(), or a break turns "find the first match" into the minimal amount of computation, which compounds into real savings on large inputs.
These five bullets are the elevator pitch for generators. Constant memory over linear data; the ability to handle streams larger than RAM; safe modeling of infinite sequences; clean multi-stage pipelines; and early exit without wasted work. Every one of them traces back to the single property of laziness from post one.
If someone asks "why would I use a generator," any of these is a complete answer. They're not five separate features so much as five faces of the same underlying behavior.
The comparison keeps you honest about the limits. Generators win when data is large, streamed, infinite, or likely to be exited early. Lists win when you need indexing, slicing, len(), reuse, or multiple passes — and when the data is small enough that none of the generator advantages apply.
The decision usually comes down to two questions: how big is the data, and how many times will I traverse it? Big and once means generator. Small or many times means list. Holding both columns in mind prevents both overuse and underuse.
Premature laziness is the mirror image of the post's enthusiasm, and it earns the mistake slot because it's a real anti-pattern. Generators add cognitive overhead: single-use semantics, no length, deferred errors, harder debugging. On a ten-element list, none of the benefits materialize and all of the costs do.
The rule is to reach for laziness when scale demands it — large files, streams, infinite sources, expensive per-item work you might skip. For ordinary small collections, a plain list is simpler, faster to reason about, and easier to debug. Match the tool to the scale rather than reaching for the fancy one reflexively.
The cover and CTA frame this as the motivation post. Having established the terms in post one and the payoff here, post three opens the hood: the iterator protocol, what next() actually does, and the mechanism by which yield suspends and resumes a function. The why naturally leads to the how.