Skip to main content

Decorators: identity, factories, class decorators

advanced22 min readLesson 112 of 169

Write decorators that preserve metadata, take arguments, and wrap classes.

Decorators beyond the basics

You know @decorator wraps a function. Advanced usage is about preserving identity, taking arguments, and decorating classes.

import functools

def timed(fn):
    @functools.wraps(fn)                # keep __name__, __doc__, __wrapped__
    def wrapper(*args, **kwargs):
        import time
        start = time.perf_counter()
        try:
            return fn(*args, **kwargs)
        finally:
            print(f"{fn.__name__}: {time.perf_counter() - start:.6f}s")
    return wrapper

Without functools.wraps, the wrapper replaces the function's metadata โ€” breaking introspection, docs, and debuggers.

Decorator factories take arguments and return the real decorator:

def retry(times):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return fn(*args, **kwargs)
                except Exception:
                    if attempt == times:
                        raise
        return wrapper
    return decorator

@retry(times=3)
def flaky(): ...

Class decorators receive and return the class โ€” a lightweight alternative to mixins or metaclasses when you only need to patch or register:

def final(cls):
    cls.__final__ = True
    return cls

@final
class Config: ...

One subtlety professionals hit in production: a decorator applied to methods must survive descriptor lookup โ€” functools.wraps copies __wrapped__, so inspect.signature still sees the original parameters. And stacking order is bottom-up: the decorator closest to the def runs first (outermost last).

Now practice

Decorator EngineeringDecorators that survive code review: identity preserved, arguments handled, class-aware.2 challenges ยท ยท ~20 min