Week 6 - Decorators, functools, and contextlib¶
6.1 Conceptual Core¶
- A decorator is just
f = decorator(f). The@is sugar. - A useful decorator preserves: name, docstring, signature, type annotations, async-ness, and
__wrapped__for introspection.functools.wrapshandles the first three; preserving signature and type requiresParamSpec(PEP 612). - Class decorators decorate the class object itself.
@dataclassis the canonical example.
6.2 Mechanical Detail¶
functools.wraps,functools.partial,functools.partialmethod,functools.lru_cache(andcachein 3.9+ for unbounded),functools.singledispatch,functools.singledispatchmethod,functools.reduce(rarely the right tool - usually a comprehension orsum).- Type-preserving decorators with
ParamSpecandTypeVar: contextlib.contextmanagerfor generator-based context managers;contextlib.asynccontextmanagerfor async.contextlib.ExitStack/AsyncExitStack: the right tool for a dynamic number of context managers (e.g., opening a list of files determined at runtime).contextlib.suppress,contextlib.closing,contextlib.redirect_stdout.
6.3 Lab - "The Retry Decorator That Doesn't Lie About Its Type"¶
- Write
@retry(times=3, on=(IOError,), backoff=0.1). Make it work on both sync and async functions (detect withasyncio.iscoroutinefunction). - Use
ParamSpecso thatpyright --strictpreserves the wrapped signature. - Add structured logging on each retry. Add a
tenacity-style backoff strategy (constant, exponential, jittered). - Compare to
tenacitylibrary; document where yours is simpler / worse / better.
6.4 Idiomatic & Linter Drill¶
- Enable
ruffFBT(boolean-trap),ARG(unused arguments). Refactor decorators to take keyword-only configuration.
6.5 Production Hardening Slice¶
- Add
mypy(in addition topyright) withstrict_optional,disallow_any_generics. The two type checkers disagree on edge cases; configuring both surfaces those.