Decorators

A track of P29 · Functions, Decorators & Generators.

From @sugar to design tool: hand-write wrappers with functools.wraps, parametrise them as three-layer factories, stack them deliberately, and write class-based decorators with __call__.

Your team's codebase has @app.route("/orders"), @lru_cache(maxsize=256), @pytest.mark.parametrize, @retry(3) — and one day a stack of two of them misbehaves: the retry logs three attempts in production but your @log above it shows only one call. Which decorator is wrapping which? If you read @ as an incantation, that bug is a wall. If you read it as what it is — a function call at def time — it is a one-line diagnosis.

That reading is this track's whole method. @a above a function executes f = a(f), once, when the def runs; the decorator receives a function and returns a replacement, usually a closure called wrapper that forwards *args, **kwargs, does its extra work, and hands the result back. You write @count_calls and @memoize from scratch — the hand-rolled heart of lru_cache — and learn the one-line habit that keeps decorated functions debuggable: functools.wraps, so the wrapper stops erasing the name, docstring and signature of what it wraps.

The second module adds the two production shapes. @retry(3) has parentheses — so retry(3) runs first and must return a decorator: a factory, three nested layers, settings captured in the closure. Stacking applies bottom-up, which is why log-outside-retry and retry-outside-log are different systems — the opening bug, resolved by reading order. And when wrapper state outgrows a counter, a class with __init__ + __call__ decorates just as well, with instance attributes for state — plus the method subtlety interviewers love: the wrapper sees self as merely its first positional argument. The track closes with a free-form interview rung that asks you to explain the whole machine end to end.

Wrap and return@a means f = a(f)functools.wrapskeep the identityFactoriessettings, then decoratorClass-based__call__ with state
Foundation first: the plain wrapper, then the shapes production code adds.