Skip to content

Decorators & metaprogramming

Senior

Writing 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

  1. 01`functools.wraps` — preserving a wrapped function's identity, and what breaks without it

    A decorator that simply returns a new inner wrapper function, without special care, replaces the original function’s identity from the perspective of introspection — the wrapper’s own __name__ (literally "wrapper"), __doc__ (likely None), 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, or functools.singledispatch’s own internals) can fail outright if what they’re inspecting is the generic wrapper’s signature rather than the real one.

    #c-functoolswraps-preserving-a-wrapped-functions-identity-and-w
  2. 02Parameterized decorators — why `@retry(times=3)` needs an extra layer of nesting

    A plain, unparameterized decorator is a function taking func and returning a wrapped_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, takes func and returns wrapped_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 to times/delay via closure over the outermost function’s arguments.

    #c-parameterized-decorators-why-retrytimes3-needs-an-extra-laye
  3. 03Stacked decorators — wraps bottom-up at definition time, executes top-down at call time

    At definition time, decorators apply from the bottom up: c wraps the original f first, producing an intermediate function; b then wraps that result; a wraps 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 invokes a’s wrapper first (since it’s the outermost layer), which — assuming it calls through as decorators typically do — calls into b’s wrapper, which calls into c’s wrapper, which finally calls the genuine, original f. 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.

    #c-stacked-decorators-wraps-bottom-up-at-definition-time-execut
  4. 04Class-based decorators (`__call__`) vs closure-based — carrying state across calls

    A 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 with nonlocal (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.

    #c-class-based-decorators-__call__-vs-closure-based-carrying-st
  5. 05Metaclasses vs `__init_subclass__` vs class decorators

    A 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 a class Foo: ... statement, before Foo even 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 — @dataclass is 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): ...                          # registered
    
    Tool 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.

    #c-metaclasses-vs-__init_subclass__-vs-class-decorators
  6. 06What happens when Python executes a `class` statement — `__prepare__`, body, metaclass `__new__`/`__init__`

    First, 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 every def/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.

    #c-what-happens-when-python-executes-a-class-statement-__prepar
  7. 07Subclass registries — `__init_subclass__` vs a metaclass, and what only the metaclass can do

    __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 of type itself 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.

    #c-subclass-registries-__init_subclass__-vs-a-metaclass-and-wha
  8. 08`__set_name__` — how a descriptor learns its own attribute name automatically

    __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 — owner is that class, and name is the attribute name the descriptor was assigned to (e.g., in x = MyDescriptor() inside a class body, the descriptor’s __set_name__ gets called with name="x"). This lets a descriptor learn its own attribute name automatically, letting you write the clean x = 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.

    #c-__set_name__-how-a-descriptor-learns-its-own-attribute-name-
  9. 09Creating a class at runtime with `type(name, bases, namespace)` — when it's genuinely needed

    Calling type(name, bases, namespace) directly (rather than only using type implicitly via a class statement) 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.

    #c-creating-a-class-at-runtime-with-typename-bases-namespace-wh
  10. 10`__getattr__` (fallback) vs `__getattribute__` (every access) — and the recursion danger

    __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., referencing self.some_attr inside 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 through object.__getattribute__(self, name) for the normal-case lookups rather than naive self.x-style attribute access from within the method itself.

    #c-__getattr__-fallback-vs-__getattribute__-every-access-and-th
  11. 11Lazy attributes via `__getattr__` vs `functools.cached_property` — what the descriptor does better

    functools.cached_property is 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.

    #c-lazy-attributes-via-__getattr__-vs-functoolscached_property-
  12. 12The `inspect` module in production code — signature inspection behind FastAPI and pytest

    inspect.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. Connecting inspect directly to real frameworks like these, rather than only “useful for introspection” in the abstract, is what ties the concept back to real, everyday tools.

    #c-the-inspect-module-in-production-code-signature-inspection-b
  13. 13`exec()`/`compile()` in legitimate metaprogramming — how `dataclasses` generates `__init__` for speed

    dataclasses (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, and execs 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.

    #c-execcompile-in-legitimate-metaprogramming-how-dataclasses-ge
  14. 14Monkey patching — legitimate uses, and the last-writer-wins risk when two libraries patch one method

    Monkey 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_method to a different function after the fact. Legitimate uses: patching specifically within tests (though unittest.mock.patch is 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.

    #c-monkey-patching-legitimate-uses-and-the-last-writer-wins-ris
  15. 15`typing.override` (PEP 698) — a static-only check that catches silent non-overrides

    Yes — typing.override (available natively from Python 3.12 onward, and earlier via the typing_extensions backport 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 the typing module (@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 @override converts into a clean, visible type-check-time error instead of a confusing, silent runtime mystery.

    #c-typingoverride-pep-698-a-static-only-check-that-catches-sile
  16. 16Catching silent non-override bugs without `typing.override` — tests, review, `abstractmethod`

    Real, 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.abstractmethod on 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.

    #c-catching-silent-non-override-bugs-without-typingoverride-tes
  17. 17A map of the standard-library decorators and what each does

    The 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 an Enum class 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.

    #c-a-map-of-the-standard-library-decorators-and-what-each-does
  18. 18`functools.cache` vs `functools.lru_cache()` — a convenience alias for `maxsize=None`

    functools.cache is genuinely, literally implemented internally as lru_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 from lru_cache itself.

    #c-functoolscache-vs-functoolslru_cache-a-convenience-alias-for
  19. 19`typing.final` (PEP 591) — static-only, with no runtime enforcement

    Genuinely static-only, following exactly the same recurring pattern seen throughout the typing module — 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 @final had never been applied at all. Assuming it raises TypeError at class-creation time is a common but incorrect assumption — this “static-only, no runtime teeth” pattern recurs across the typing module and is worth internalizing as a general fact about the module, not a one-off surprise.

    #c-typingfinal-pep-591-static-only-with-no-runtime-enforcement
  20. 20Runtime-enforcing "no subclassing" or "no override of this method" via `__init_subclass__`

    For blocking subclassing entirely: override __init_subclass__ on the class to unconditionally raise an exception — this genuinely, actively fires and raises TypeError the moment anyone attempts class 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.

    #c-runtime-enforcing-no-subclassing-or-no-override-of-this-meth