Academy
ProgrammingPython IntermediateIterators & Generators

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 for loop calls iter() then repeatedly calls next() until StopIteration
  • An iterator is any object with __iter__ and __next__ methods
  • A generator function uses yield to 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 from delegates 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.