Python Generators & Iterators Cheat Sheet
Lazy iteration in Python covering generator functions, yield from, the iterator protocol, and commonly used itertools functions.
Generator Functions (yield)
Functions that produce a lazy sequence of values.
def count_up_to(n): i = 1 while i <= n: yield i i += 1for num in count_up_to(5): print(num) # 1 2 3 4 5gen = count_up_to(3)next(gen) # 1next(gen) # 2
Generator Expressions
A compact, lazily-evaluated alternative to list comprehensions.
squares = (x ** 2 for x in range(10))next(squares) # 0list(squares) # remaining values: [1, 4, 9, ..., 81]# memory-efficient aggregation, no intermediate list builttotal = sum(x ** 2 for x in range(1_000_000))
yield from & Delegation
Delegating iteration to a sub-generator or iterable.
def chain(*iterables): for it in iterables: yield from itlist(chain([1, 2], [3, 4], (5, 6)))# [1, 2, 3, 4, 5, 6]def flatten(nested): for item in nested: if isinstance(item, list): yield from flatten(item) else: yield item
The Iterator Protocol (__iter__/__next__)
Building a custom class that supports the for loop.
class Counter: def __init__(self, limit): self.limit = limit self.n = 0 def __iter__(self): return self def __next__(self): if self.n >= self.limit: raise StopIteration self.n += 1 return self.nfor x in Counter(3): print(x) # 1 2 3
Useful itertools Functions
Standard-library building blocks for iterator composition.
- count(start=0, step=1)- infinite arithmetic sequence, e.g. count(10) -> 10, 11, 12, ...
- cycle(iterable)- repeats the iterable indefinitely
- chain(*iterables)- iterates through multiple iterables as one sequence
- islice(iterable, stop)- slices an iterator lazily, like list slicing
- zip_longest(a, b)- zips iterables, filling missing values with fillvalue
- groupby(iterable, key)- groups consecutive elements sharing a key
- product(a, b)- Cartesian product, equivalent to nested for loops
Two-Way Communication: send(), throw(), close()
Generators can receive values and exceptions from the caller, not just produce them.
def echo(): while True: try: received = yield print(f"got: {received}") except ValueError: print("ValueError injected!")gen = echo()next(gen) # prime the generator to the first yieldgen.send("hi") # prints 'got: hi'gen.throw(ValueError) # prints 'ValueError injected!'gen.close() # raises GeneratorExit inside, generator stops cleanly
Building Context Managers with @contextmanager
A generator with a single yield becomes a context manager: setup before yield, teardown after.
from contextlib import contextmanagerimport time@contextmanagerdef timer(label): start = time.perf_counter() try: yield finally: print(f"{label}: {time.perf_counter() - start:.4f}s")with timer("block"): sum(range(1_000_000))# prints elapsed time even if an exception occurred inside the with block
Async Generators (async def + yield)
Combine async/await with yield to lazily produce values across await points.
import asyncioasync def fetch_pages(urls): for url in urls: await asyncio.sleep(0.1) # simulate I/O yield f"contents of {url}"async def main(): async for page in fetch_pages(["a", "b", "c"]): print(page)# asyncio.run(main())# note: list comprehensions have an async form too:# results = [p async for p in fetch_pages(urls)]
Generator Pipelines
Chaining generators lazily processes a stream without materializing intermediate lists.
def read_lines(path): with open(path) as f: for line in f: yield line.rstrip("\n")def non_empty(lines): for line in lines: if line.strip(): yield linedef parse_ints(lines): for line in lines: yield int(line)# pipeline: each stage pulls one item at a time, constant memory# total = sum(parse_ints(non_empty(read_lines("numbers.txt"))))
Advanced Generator & Iterator Notes
Behaviors and stdlib tools that matter once you go past basic for-loop consumption.
- PEP 479 (StopIteration handling)- since Python 3.7, a StopIteration raised accidentally inside a generator body is converted to a RuntimeError instead of silently ending iteration
- itertools.tee(iterable, n)- splits one iterator into n independent iterators; the original should not be used directly afterward
- iter(callable, sentinel)- the two-argument form of iter() repeatedly calls callable() until it returns sentinel, useful for reading fixed-size chunks
- Generator return value- a `return expr` inside a generator sets StopIteration.value, retrievable via `yield from` or by catching StopIteration manually
- typing.Generator[YieldT, SendT, ReturnT]- the type-hint form for annotating a generator function's yield, send, and return types
- Generators are not reentrant- calling next() on a generator that is already executing (e.g. recursively) raises ValueError: generator already executing
A generator can only be iterated once — once exhausted (or partially consumed with next()), it cannot be reset. If you need to iterate the same data multiple times, convert it to a list or re-create the generator.