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 / COMMITLearn this free with Aria, your AI tutor → AiCanCode.org/learn/python