Decorators & metaprogramming
SeniorWriting decorators that introspect cleanly, metaclasses vs __init_subclass__, dynamic attribute access, code generation, @override and @final.
Created 12 September 2026 · Updated 23 October 2026
20 concepts · detail hidden
01`functools.wraps` — preserving a wrapped function's identity, and what breaks without it
#c-functoolswraps-preserving-a-wrapped-functions-identity-and-wA decorator that simply returns a new inner
wrapperfunction, without special care, replaces the original function’s identity from the perspective of introspection — the wrapper’s own__name__(literally"wrapper"),__doc__(likelyNone), and signature end up shadowing the original function’s, since that’s genuinely what got returned from the decoration.functools.wraps(original_func), applied to the wrapper, copies over the original’s metadata (and sets a__wrapped__attribute pointing back to it) so introspection tools see through the decoration correctly. This matters well beyond cosmetics:help()becomes useless/misleading without it; any tooling that keys behavior off a function’s__name__(web frameworks routing by function name, logging that labels entries by function name) silently breaks or misbehaves; and tools that inspect a function’s actual signature (like FastAPI’s dependency injection, orfunctools.singledispatch’s own internals) can fail outright if what they’re inspecting is the generic wrapper’s signature rather than the real one.02Parameterized decorators — why `@retry(times=3)` needs an extra layer of nesting
#c-parameterized-decorators-why-retrytimes3-needs-an-extra-layeA plain, unparameterized decorator is a function taking
funcand returning awrapped_func. Once you want to pass configuration to the decorator itself (@retry(times=3, delay=1)), you actually need a decorator factory: a function that takes the configuration arguments (times,delay) and returns the actual decorator (which then, as before, takesfuncand returnswrapped_func) — three levels of nested functions in total. Tracing it precisely:retry(times=3, delay=1)is called first (at decoration time, with the configuration), returning the real decorator; that decorator is then immediately applied to the function being decorated, returning the final wrapped function; only later, at actual call time, does the innermost wrapper logic run, now with access totimes/delayvia closure over the outermost function’s arguments.03Stacked decorators — wraps bottom-up at definition time, executes top-down at call time
#c-stacked-decorators-wraps-bottom-up-at-definition-time-executAt definition time, decorators apply from the bottom up:
cwraps the originalffirst, producing an intermediate function;bthen wraps that result;awraps the result of that — so the order of wrapping, reading the stack top-to-bottom in source, is actually applied bottom-to-top. At call time, execution runs the opposite way, top-down: calling the final decorated name invokesa’s wrapper first (since it’s the outermost layer), which — assuming it calls through as decorators typically do — calls intob’s wrapper, which calls intoc’s wrapper, which finally calls the genuine, originalf. This “wraps bottom-up, executes top-down” pattern is easy to get backwards without explicitly tracing through it, even for people who use stacked decorators regularly.04Class-based decorators (`__call__`) vs closure-based — carrying state across calls
#c-class-based-decorators-__call__-vs-closure-based-carrying-stA class implementing
__call__can be used directly as a decorator — the class instance itself takes the place of the “wrapper function.” A class-based decorator maintaining state (e.g., a call counter as an instance attribute, incremented every time__call__runs) makes that persistent state explicit and directly inspectable as regular attributes on the decorator instance, rather than needing closures withnonlocal(or a mutable container as a workaround) to carry state across calls the way a purely function-based decorator would. It’s also naturally easier to expose additional behavior beyond just “call the wrapped function” — e.g., a.reset()method on the decorator instance itself — something a closure-based decorator has no clean, equivalent way to expose to callers.05Metaclasses vs `__init_subclass__` vs class decorators
#c-metaclasses-vs-__init_subclass__-vs-class-decoratorsA metaclass is “the class of a class” — just as an ordinary object’s class determines its behavior, a class’s metaclass (by default, the built-in
type) determines how that class itself behaves, including how it’s constructed. Writing a custom metaclass lets you hook into and customize the class-creation process itself — intercepting what happens when Python executes aclass Foo: ...statement, beforeFooeven fully exists. Historically this was the go-to tool for things like automatically registering every subclass of a base class into a lookup table (a “plugin registry” pattern), enforcing that subclasses implement certain methods, or injecting behavior uniformly across a whole class hierarchy. Metaclasses are needed far less often today than a lot of Python folklore suggests, because Python added narrower, purpose-built tools that cover the overwhelming majority of “I think I need a metaclass” situations:__init_subclass__(a special classmethod, added in Python 3.6, that automatically runs whenever a subclass is defined — perfect for registries and enforcing subclass contracts, without the ceremony of a full metaclass),__set_name__(lets a descriptor learn its own attribute name automatically), and plain class decorators (a function that takes a class and returns a modified one, run once at definition time, syntactically simpler than a metaclass). In practice, a metaclass is rarely, if ever, genuinely needed in production — it remains the right tool mainly for edge cases like needing to intercept the class namespace while the class body is still executing (via__prepare__), which none of the narrower alternatives can do.Class decorators — one-time modification, no propagation
A class decorator is just a function that receives a class object and returns a (possibly modified) class, applied with the same
@syntax as function decorators —@dataclassis exactly this pattern. It runs once, when the decorated class is defined, and its effect does not propagate to subclasses: a subclass that isn’t itself decorated never re-triggers the parent’s decorator. A metaclass, by contrast, governs the class-creation process for every class whose metaclass it is — and a subclass inherits its parent’s metaclass — so its effect propagates through the entire hierarchy automatically;__init_subclass__propagates the same way, since it’s inherited like any classmethod.registry = {} def register(cls): # class decorator: runs once, for this class only registry[cls.__name__] = cls return cls @register class Base: ... class Child(Base): ... # NOT registered — decorator isn't inherited class AutoBase: def __init_subclass__(cls, **kw): # runs for every subclass, at any depth super().__init_subclass__(**kw) registry[cls.__name__] = cls class A(AutoBase): ... # registered class B(A): ... # registeredTool Runs when Applies to subclasses Can see the class body while it executes Class decorator once, after the decorated class is built no (must re-decorate) no __init_subclass__after each subclass is built yes no Metaclass around every class it creates ( __prepare__,__new__,__init__,__call__)yes yes, via __prepare__A class decorator is very often the right-sized tool for problems people reflexively reach for a metaclass to solve — one-time post-processing of a single class. Use
__init_subclass__when the behaviour must apply hierarchy-wide, and a metaclass only for what the other two cannot do.06What happens when Python executes a `class` statement — `__prepare__`, body, metaclass `__new__`/`__init__`
#c-what-happens-when-python-executes-a-class-statement-__preparFirst,
type.__prepare__(or the equivalent on a custom metaclass) is called, returning the namespace — a dict-like mapping — that the class body’s code is about to execute into, rather than the class body creating its own fresh, ordinary dict from scratch. The class body then genuinely executes as ordinary Python code, with everydef/assignment inside it populating that namespace object as it goes (this is why, mechanically, a class body really is just executable code, not a separate declarative syntax). Once the body finishes executing, the metaclass’s__new__receives that fully-populated namespace and is responsible for actually constructing the class object itself from it;__new__returns that class object, and then the metaclass’s__init__runs to perform any further initialization on the already-constructed class. You’d override__prepare__specifically when you need an ordered or custom mapping capturing something about the class body’s execution that a plain dict wouldn’t preserve — the classic historical motivation was capturing attribute definition order before Python 3.7 guaranteed dict ordering by default, though the underlying “custom namespace capture” pattern still has other legitimate specialized uses today.07Subclass registries — `__init_subclass__` vs a metaclass, and what only the metaclass can do
#c-subclass-registries-__init_subclass__-vs-a-metaclass-and-wha__init_subclass__(a special classmethod, automatically invoked whenever a new subclass is defined, added in Python 3.6) is dramatically simpler for this exact use case — a plugin registry, enforcing that subclasses implement certain methods, or injecting shared behavior into every subclass — and covers the large majority of situations people historically reached for a full custom metaclass to solve. Genuine remaining edges where a metaclass is still necessary: needing to intercept/customize the class namespace during the class body’s execution itself (via__prepare__—__init_subclass__only runs after the class is already fully constructed, too late for this); needing to actually change the metaclass oftypeitself or otherwise operate at that deeper level; or needing to control instance-creation behavior uniformly across an entire class hierarchy in ways tied specifically to class construction rather than subclass definition. It’s fair to say the metaclass version is essentially never needed in practice today, but these edges are still worth knowing rather than treating metaclasses as purely obsolete.08`__set_name__` — how a descriptor learns its own attribute name automatically
#c-__set_name__-how-a-descriptor-learns-its-own-attribute-name-__set_name__(self, owner, name)is a special method automatically called on a descriptor object at the moment the class it’s being assigned to as a class attribute is itself being created —owneris that class, andnameis the attribute name the descriptor was assigned to (e.g., inx = MyDescriptor()inside a class body, the descriptor’s__set_name__gets called withname="x"). This lets a descriptor learn its own attribute name automatically, letting you write the cleanx = MyDescriptor()form, instead of the older, more error-prone pattern of manually passing the name as a constructor argument (x = MyDescriptor("x")), which could silently get out of sync if someone renamed the attribute on one side but forgot to update the matching string on the other.09Creating a class at runtime with `type(name, bases, namespace)` — when it's genuinely needed
#c-creating-a-class-at-runtime-with-typename-bases-namespace-whCalling
type(name, bases, namespace)directly (rather than only usingtypeimplicitly via aclassstatement) constructs a genuinely new class object at runtime, entirely programmatically — passing the class’s desired name as a string, a tuple of base classes, and a dict for its namespace/attributes. Real, legitimate scenarios: an ORM (object-relational mapper) generating model classes dynamically from a database schema discovered at runtime, rather than requiring every table to have a hand-written class; a testing framework programmatically generating parametrized test-case classes; or a plugin system assembling a class’s shape based on external configuration read at startup. The useful distinction is genuine necessity (the class’s shape genuinely isn’t known until runtime) versus a clever-looking way to avoid writing an ordinary class definition — the latter is usually an unnecessary complexity smell rather than a real requirement.10`__getattr__` (fallback) vs `__getattribute__` (every access) — and the recursion danger
#c-__getattr__-fallback-vs-__getattribute__-every-access-and-th__getattr__is only invoked as a fallback — Python calls it only after normal attribute lookup has already failed to find the attribute through the usual channels (instance__dict__, class__dict__/MRO).__getattribute__, by contrast, intercepts every single attribute access on the object, full stop — including ones that would have succeeded perfectly normally without any custom logic at all. This makes overriding__getattribute__genuinely dangerous: a bug in your override (a very easy one to write by accident — e.g., referencingself.some_attrinside your own__getattribute__implementation, which recursively triggers__getattribute__again for that very access, infinitely) causes catastrophic infinite recursion, since there’s no natural “fallback path” the way__getattr__inherently has; correctly written overrides of__getattribute__must carefully route throughobject.__getattribute__(self, name)for the normal-case lookups rather than naiveself.x-style attribute access from within the method itself.11Lazy attributes via `__getattr__` vs `functools.cached_property` — what the descriptor does better
#c-lazy-attributes-via-__getattr__-vs-functoolscached_property-functools.cached_propertyis implemented as a proper non-data descriptor — on first access, its__get__computes the value and then explicitly stores that computed result directly into the instance’s own__dict__under the same attribute name; because it’s a non-data descriptor (no__set__), that instance__dict__entry then shadows the descriptor entirely on every subsequent access (per the usual descriptor-priority rules), meaning later lookups find the cached value directly with no repeated function call or descriptor invocation at all — genuinely fast, and structurally impossible to “forget to cache,” since the shadowing happens automatically as a side effect of the descriptor protocol itself. A hand-rolled__getattr__-based version can achieve the same effect, but it’s easy to get subtly wrong — e.g., forgetting to actually persist the computed value somewhere durable, silently recomputing on every single access and defeating the entire point of “caching” without an obvious, immediate symptom that anything’s wrong.12The `inspect` module in production code — signature inspection behind FastAPI and pytest
#c-the-inspect-module-in-production-code-signature-inspection-binspect.signature()lets code programmatically examine a function’s actual parameter names, their declared types, and default values — this is genuinely production-grade, load-bearing machinery, not just a debugging convenience: it’s literally how FastAPI’s dependency injection determines what to automatically supply to a route handler’s parameters, and how pytest’s fixture injection figures out which fixtures a given test function needs, purely by inspecting the function’s own signature rather than requiring explicit, manual wiring for every single function. Connectinginspectdirectly to real frameworks like these, rather than only “useful for introspection” in the abstract, is what ties the concept back to real, everyday tools.13`exec()`/`compile()` in legitimate metaprogramming — how `dataclasses` generates `__init__` for speed
#c-execcompile-in-legitimate-metaprogramming-how-dataclasses-gedataclasses(the standard library module) is a genuinely great real-world example: it doesn’t implement__init__as one generic, reflective function looping over a list of declared fields at every instantiation — instead, at class-definition time, it actually generates the real Python source code text for a field-specific__init__(and__repr__,__eq__, etc.), tailored exactly to that particular class’s declared fields, andexecs that generated source once to produce a genuine, compiled function — specifically for performance: a compiled, field-specific function executing directly is meaningfully faster than an equivalent generic, reflective implementation that has to loop over field metadata on every single instantiation.14Monkey patching — legitimate uses, and the last-writer-wins risk when two libraries patch one method
#c-monkey-patching-legitimate-uses-and-the-last-writer-wins-risMonkey patching means modifying or replacing an existing class’s or module’s behavior at runtime, from outside its own original definition — e.g., reassigning
SomeClass.some_methodto a different function after the fact. Legitimate uses: patching specifically within tests (thoughunittest.mock.patchis the sanctioned, properly-scoped-and-restored version of this rather than raw ad-hoc patching); temporarily working around a known bug in a third-party library you can’t immediately fix upstream yourself; or adding a compatibility shim bridging behavior differences between library versions. The real risk profile: if two separate libraries both attempt to monkey patch the exact same method, the behavior is effectively last-writer-wins, with no coordination or warning between them — whichever patch was applied most recently silently wins, potentially and invisibly discarding the other library’s intended behavior entirely; patches also frequently don’t survive a library upgrade that changes the internal implementation details being patched against, breaking silently; and debugging code affected by monkey patching is often genuinely miserable, since the patched behavior isn’t visible anywhere at the actual call site — you have to already know a patch exists somewhere else in the codebase to even think to look for one.15`typing.override` (PEP 698) — a static-only check that catches silent non-overrides
#c-typingoverride-pep-698-a-static-only-check-that-catches-sileYes —
typing.override(available natively from Python 3.12 onward, and earlier via thetyping_extensionsbackport package) is a decorator applied to a method specifically to declare “this method is intentionally overriding a method inherited from a base class.” Consistent with a recurring pattern in thetypingmodule (@overload,Final), it’s genuinely static-only: a type checker like mypy or pyright verifies that a genuinely matching method exists somewhere in the class’s own MRO, and flags an error if it doesn’t — at runtime, the decorator itself does essentially nothing beyond quietly setting an internal__override__marker attribute, meaning misuse produces no runtime error whatsoever on its own. It’s genuinely valuable despite being purely static: the real bug it catches is a base class method later getting renamed or removed entirely, which silently turns what a developer thought was a working override into a completely unrelated new method that simply never gets called polymorphically at all — a genuinely nasty, entirely silent class of bug in large class hierarchies, which@overrideconverts into a clean, visible type-check-time error instead of a confusing, silent runtime mystery.16Catching silent non-override bugs without `typing.override` — tests, review, `abstractmethod`
#c-catching-silent-non-override-bugs-without-typingoverride-tesReal, practical mitigations: dedicated naming/interface tests explicitly asserting that a subclass’s method signature genuinely matches its base class’s expected signature; disciplined code review specifically watching for this class of mistake; or, as a decent partial substitute, using
abc.abstractmethodon the base class, which at least genuinely enforces that some override is present in any concrete subclass, even though it can’t verify that override’s signature actually matches what was intended. This specific class of bug is a familiar frustration on any pre-3.12 codebase.17A map of the standard-library decorators and what each does
#c-a-map-of-the-standard-library-decorators-and-what-each-doesThe standard library’s decorator surface is broader than it first looks:
@property/@x.setter/@x.deleter;@staticmethod/@classmethod;@functools.wraps;@functools.lru_cache/@functools.cache(the 3.9+ unbounded-cache shorthand);@functools.singledispatch/@functools.singledispatchmethod;@functools.total_ordering;@functools.cached_property;@dataclasses.dataclass;@abc.abstractmethod;@contextlib.contextmanager/@contextlib.asynccontextmanager;@typing.overload;@typing.final;@typing.override;@typing.runtime_checkable;@atexit.register(registers a process-exit cleanup hook — a real, if unusual, production tool);@enum.unique(raises an error if anEnumclass accidentally defines two members sharing the exact same underlying value). Worth mentioning even though third-party rather than standard library:@unittest.mock.patch,@pytest.fixture,@pytest.mark.parametrize. These decorators cut across nearly every topic in this document, from class construction to caching to typing enforcement.18`functools.cache` vs `functools.lru_cache()` — a convenience alias for `maxsize=None`
#c-functoolscache-vs-functoolslru_cache-a-convenience-alias-forfunctools.cacheis genuinely, literally implemented internally aslru_cache(maxsize=None)— meaning unbounded caching with no eviction at all — so it’s purely a convenient shorthand for the specific, extremely common case of “cache everything indefinitely, never evict anything,” not a fundamentally different caching mechanism fromlru_cacheitself.19`typing.final` (PEP 591) — static-only, with no runtime enforcement
#c-typingfinal-pep-591-static-only-with-no-runtime-enforcementGenuinely static-only, following exactly the same recurring pattern seen throughout the
typingmodule — a type checker flags, as an error, either a class that subclasses a@final-decorated class, or a subclass that overrides a@final-decorated method; at runtime, though, absolutely nothing prevents it — the subclass is genuinely created successfully, and the override genuinely takes effect completely normally, exactly as though@finalhad never been applied at all. Assuming it raisesTypeErrorat class-creation time is a common but incorrect assumption — this “static-only, no runtime teeth” pattern recurs across thetypingmodule and is worth internalizing as a general fact about the module, not a one-off surprise.20Runtime-enforcing "no subclassing" or "no override of this method" via `__init_subclass__`
#c-runtime-enforcing-no-subclassing-or-no-override-of-this-methFor blocking subclassing entirely: override
__init_subclass__on the class to unconditionally raise an exception — this genuinely, actively fires and raisesTypeErrorthe moment anyone attemptsclass Sub(MyFinalClass): ..., providing real, actual runtime enforcement with no type checker required at all. Blocking override of just one specific method, while still permitting subclassing more generally, is genuinely trickier and considerably less commonly attempted:__init_subclass__can inspect the newly-created subclass’s own namespace/__dict__directly, checking specifically for the presence of the protected method name, and raise if the subclass has genuinely redefined it — this works, but requires a hand-rolled check rather than any built-in language keyword providing it directly.