Skip to main content

Generators: Lazy Sequences

intermediate14 min readLesson 68 of 169

yield turns a function into a streaming factory โ€” memory-friendly by design.

A generator function contains yield. Calling it doesn't run the body โ€” it returns a generator that produces values lazily, one at a time:

def read_numbers(lines):
    for line in lines:
        line = line.strip()
        if line.isdigit():
            yield int(line)        # pause here, hand out one value

gen = read_numbers(["1", "oops", "3"])
next(gen)      # 1
next(gen)      # 3 โ€” the bad line was skipped inside the function

Two superpowers come from this laziness:

  • Constant memory: a generator never materializes the whole sequence. This streams a 10 GB log through a 1 MB loop without breaking a sweat.
  • Composable pipelines: generators feed generators, and each stage stays simple.
def sum_big_sales(rows):
    big = (r for r in rows if r["amount"] > 1000)   # generator expression
    return sum(r["amount"] for r in big)

yield from and delegation

A generator can delegate to another iterable with yield from:

def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)     # recursively hand out each element
        else:
            yield item

When NOT to use generators

If you need the data more than once (len(), indexing, re-iteration), build a list. Generators are single-use streams โ€” reach for them when data is large, infinite, or produced on demand.

Now practice

Generator DrillsStream, filter lazily, and flatten nested data.3 challenges ยท ยท ~30 min