Iterators & Generators
What Actually Happens in a for Loop
You've used for loops since the beginner series. But what exactly does Python do when you write:
for item in collection:
The answer involves two concepts: iterables and iterators.
- An iterable is any object that can be looped over — a list, a string, a tuple, a dictionary.
- An iterator is an object that knows how to produce the next value on demand, one at a time.
When Python sees a for loop, it calls iter() on the collection to get an iterator, then repeatedly calls next() on that iterator until it's exhausted.
Each next() call advances the iterator by one step. The for loop is simply calling next() for you, invisibly, until a StopIteration exception signals the end.
StopIteration is the signal that the sequence is exhausted. The for loop catches this silently and stops. You've been relying on this mechanism in every loop you've written.
Writing Your Own Iterator
Any class can become an iterator by implementing two methods: __iter__ and __next__.
__iter__ returns the iterator object itself. __next__ returns the next value — or raises StopIteration when done. The for loop handles the rest.
This works, but writing a class with two dunder methods for every custom sequence is verbose. Python gives you a far cleaner tool.
Generators — The Elegant Solution
A generator is a function that uses yield instead of return. Each time yield is encountered, the function pauses, sends the value out, and resumes from exactly that point on the next call.
The function looks like it runs to completion, but it doesn't. Every yield suspends execution. The local variable start is preserved between calls. The next next() call resumes right after the yield.
This achieves exactly what the Countdown class did — in a fraction of the code.
Generators Are Lazy
The critical difference between a generator and a list: a generator produces values only when asked for them. It doesn't compute everything upfront.
gen holds a generator object — not a list of squares. Nothing is computed until next() is called. The square is calculated on demand, one at a time.
This laziness is the point. A list of a million squares requires memory for a million integers. A generator requires memory for exactly one — the current value.
An infinite sequence — something a list can never be. The generator can run forever because it only ever holds one value at a time. You control how many you take from it.
Generator Expressions
Just as list comprehensions are compact list-builders, generator expressions are compact generators:
The only syntactic difference: square brackets [] for a list comprehension, parentheses () for a generator expression.
When you only need to iterate once and don't need all values in memory simultaneously, prefer the generator expression.
A Practical Generator
Processing large text in chunks — one piece at a time, without loading the entire content at once. This pattern scales directly to reading large files, processing network streams, and handling data that won't fit in memory.
yield from — Delegating to Another Iterable
yield from lets a generator delegate to another iterable, producing all its values in sequence:
yield from lst is shorthand for for item in lst: yield item. Clean and readable when composing iterables.
What You Have Learned
Iterators and generators reveal the machinery behind Python's most fundamental loop — and give you a powerful tool for building memory-efficient sequences.
The key ideas:
- Every
forloop callsiter()then repeatedly callsnext()untilStopIteration - An iterator is any object with
__iter__and__next__methods - A generator function uses
yieldto pause and resume execution, preserving state between calls - Generators are lazy — values are produced on demand, not upfront
- Generator expressions
(expr for item in iterable)are compact inline generators - Use generators when sequences are large, potentially infinite, or only consumed once
yield fromdelegates to another iterable cleanly
You've Completed the Intermediate Series
Ten lessons covering list comprehensions, tuples and sets, advanced functions, scope and closures, error handling, file I/O, the standard library, object-oriented programming, and generators.
You can now write structured, real-world Python programs — with proper error handling, reusable classes, clean abstractions, and memory-efficient data pipelines.
The Advanced series begins where this one ends: decorators, context managers, concurrency, NumPy, and beyond.