Skip to content

Decorators#

Decorators wrap functions (or classes) to extend behavior without modifying source. They are syntactic sugar for func = decorator(func) and are built on Closures.

How to use this page

Master closures first. Used in timing, logging, auth, caching, and validation patterns.

At a glance
Track Python Advanced → Advanced Functions
Sections 7 major topics
Outline Use the right-hand TOC to jump

Topics: What a decorator is · Preserving metadata — functools.wraps · Decorators with arguments (three levels) · Common decorator patterns · Stacking decorators · Class decorators · staticmethod / classmethod as decorators

  1. What a decorator is
  2. Preserving metadata — functools.wraps
  3. Decorators with arguments (three levels)
  4. Common decorator patterns
  5. Stacking decorators
  6. Class decorators
  7. staticmethod / classmethod as decorators

What a decorator is#

def add_logging(fn):
    def wrapper(*args, **kwargs):
        print(f"calling {fn.__name__}")
        return fn(*args, **kwargs)
    return wrapper

@add_logging
def greet(name: str):
    print(f"Hello, {name}")

# Equivalent to: greet = add_logging(greet)
greet("Alice")

The @ syntax applies decorator at function definition time (when module loads).


Preserving metadata — functools.wraps#

Bare wrappers hide original name/docstring:

from functools import wraps

def add_logging(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        print(f"calling {fn.__name__}")
        return fn(*args, **kwargs)
    return wrapper

greet.__name__   # 'greet' with wraps; 'wrapper' without

Always use @wraps(fn) on wrapper functions.


Decorators with arguments (three levels)#

from functools import wraps

def repeat(times: int):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = fn(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say(msg: str):
    print(msg)

Pattern: outer takes decorator config → middle takes fninner is wrapper.


Common decorator patterns#

Timing#

import time
from functools import wraps

def timer(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return fn(*args, **kwargs)
        finally:
            print(f"{fn.__name__}: {time.perf_counter() - start:.4f}s")
    return wrapper

Caching#

from functools import cache, lru_cache

@cache
def fib(n: int) -> int:
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

@lru_cache(maxsize=128)
def expensive(a: int, b: int) -> int:
    ...

Prefer stdlib @cache over hand-rolled memoization.

Validation#

def require_positive(fn):
    @wraps(fn)
    def wrapper(x: int, *args, **kwargs):
        if x <= 0:
            raise ValueError("x must be positive")
        return fn(x, *args, **kwargs)
    return wrapper

Register / plugin pattern#

REGISTRY: dict[str, callable] = {}

def register(name: str):
    def decorator(fn):
        REGISTRY[name] = fn
        return fn
    return decorator

@register("sum")
def add(a, b):
    return a + b

Stacking decorators#

Applied bottom-up (closest to def first):

@decorator_a
@decorator_b
def f():
    pass

# f = decorator_a(decorator_b(f))

Order matters when decorators transform signatures or side effects.


Class decorators#

def singleton(cls):
    instances = {}
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    pass

Also: @dataclass, @total_ordering. See Class Decorators.


staticmethod / classmethod as decorators#

Built-in decorators change descriptor behavior on classes — different mechanism from function wrappers.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
Missing @wraps Lost __name__, __doc__ Always wrap with wraps
Decorator order Wrong application order Remember bottom-up
Stateful decorator Shared mutable default New dict inside factory
@cache on mutable args Unhashable TypeError Tuple args
Decorator changes signature Breaks introspection Use wraps, consider typing.ParamSpec

Mental model checklist#

  1. What does @deco desugar to?
  2. Why three nested functions for parameterized decorators?
  3. What does @wraps preserve?
  4. In @a @b def f, which runs first?
  5. When should you use @cache vs custom memo?

What's next#

Topic Page
Closures Closures
Partial functions Partial Functions
Class decorators Class Decorators
Callable objects Callable Objects