Cheat SheetsPython A–ZIterators & Decorators

Iterators & Decorators — Cheat Sheet

Python A–Z · 4 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Iterators & Decorators
Python A–Z4 topicsQuick revision reference
1

The Iteration Protocol — iter() & next()

Every for loop is sugar over iter() and next(): an iterable produces an iterator, the iterator yields items until StopIteration — implement two dunders and your class joins in.

  • Iterable = has __iter__; iterator = also has __next__ and exhausts
  • for = iter() once + next() until StopIteration
  • next(it, default) avoids the exception
  • zip/map/filter/file objects are one-shot iterators — materialize with list() if you need reuse
iter/next/StopIteration — the machinery under for
nums = [10, 20, 30]

it = iter(nums)          # list (iterable) -> list_iterator
print(next(it))          # 10
print(next(it))          # 20
print(next(it))          # 30
# next(it)               # StopIteration!
print(next(it, "END"))   # 'END' — default instead of raising

# The for loop, desugared:
it = iter(nums)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
    print(x)

# Iterators exhaust — a big source of bugs
pairs = zip([1, 2], ["a", "b"])
print(list(pairs))       # [(1,'a'), (2,'b')]
print(list(pairs))       # [] — already consumed!
2

Generators — yield & Lazy Pipelines

A function with yield returns a generator that produces values on demand, pausing between them — constant-memory processing and composable data pipelines.

  • Calling a generator function runs nothing — iteration does
  • State (locals + position) persists between next() calls; generators are one-shot
  • yield from delegates to sub-generators (recursive flattening, refactoring)
  • Pipelines of generators process unlimited data in constant memory
Generators run on demand and remember where they were
def demo():
    print("A: started")
    yield 1
    print("B: resumed")
    yield 2
    print("C: finishing")

g = demo()               # NOTHING prints — no code ran
print(next(g))           # A: started   -> 1
print(next(g))           # B: resumed   -> 2
# next(g)                # C: finishing -> StopIteration

# Infinite generator — impossible as a list
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

from itertools import islice
print(list(islice(fibonacci(), 8)))   # [0,1,1,2,3,5,8,13]

# yield from — delegate to a sub-generator
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)   # recursion, lazily
        else:
            yield item

print(list(flatten([1, [2, [3, 4]], 5])))   # [1,2,3,4,5]
3

Decorators — Wrapping Functions with Functions

@decorator replaces a function with a wrapped version — the mechanism behind logging, timing, caching, auth checks, and every @app.get you'll write in FastAPI.

  • @deco is exactly f = deco(f) — a function replaced by its wrapped version
  • Always @wraps(func) the wrapper — preserves __name__/__doc__ for debugging
  • Parameterized decorators are factories: retry(3) returns the decorator
  • Stacked decorators apply bottom-up
The universal decorator template
import time
from functools import wraps

def timed(func):
    @wraps(func)                          # keep func's name/docs
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)    # call the original
        ms = (time.perf_counter() - start) * 1000
        print(f"{func.__name__} took {ms:.1f} ms")
        return result
    return wrapper

@timed                                    # slow_sum = timed(slow_sum)
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)      # slow_sum took ~250 ms
print(slow_sum.__name__)  # 'slow_sum' — thanks to @wraps
                          # without it: 'wrapper' (breaks debugging)
4

Context Managers — with, __enter__/__exit__ & contextlib

The with statement guarantees setup/teardown around a block — files, locks, DB transactions — via __enter__/__exit__ or the @contextmanager generator shortcut.

  • with = __enter__ before the block, __exit__ after — even on exceptions
  • __exit__ returning True swallows the exception; False propagates it
  • @contextmanager: setup before yield, teardown after, wrapped in try/finally
  • Use for anything acquire/release: files, locks, transactions, timers, temp state
Transactions — the canonical enter/exit example
class Transaction:
    def __init__(self, db):
        self.db = db
    def __enter__(self):
        print("BEGIN")
        return self.db                     # bound to 'as' target
    def __exit__(self, exc_type, exc, tb):
        if exc_type is None:
            print("COMMIT")
        else:
            print(f"ROLLBACK ({exc})")
        return False                       # False = re-raise if error

class FakeDB:
    def save(self, x): print("saved", x)

try:
    with Transaction(FakeDB()) as db:
        db.save("order-1")
        raise ValueError("payment failed")
except ValueError:
    pass
# BEGIN / saved order-1 / ROLLBACK (payment failed)

with Transaction(FakeDB()) as db:
    db.save("order-2")
# BEGIN / saved order-2 / COMMIT
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/python