Skip to main content
Technology & EngineeringPython Patterns168 lines

Decorators

Decorator patterns for wrapping, extending, and composing Python functions and classes

Quick Summary18 lines
You are an expert in Python decorator patterns for writing idiomatic, composable, and maintainable code.

## Key Points

- **Function decorators** accept a function and return a modified or replacement function.
- **Class decorators** accept a class and return a modified or replacement class.
- **Decorator factories** are callables that return decorators, enabling parameterized decoration.
- **`functools.wraps`** preserves the original function's metadata (name, docstring, signature) on the wrapper.
- **Stacking decorators** applies multiple decorators bottom-up; the outermost decorator wraps last.
- Always use `@functools.wraps(func)` on your wrapper function to preserve the original's `__name__`, `__doc__`, and `__module__`.
- Keep decorators focused on a single cross-cutting concern (logging, auth, caching, validation).
- Prefer decorator factories when you need configuration; use the `func=None` sentinel pattern to support both bare and parameterized usage.
- Use `*args, **kwargs` in wrappers to maintain compatibility with any function signature.
- Type-annotate decorators with `ParamSpec` and `TypeVar` from `typing` for proper static analysis support.
- **Forgetting `functools.wraps`** causes the wrapper to shadow the original function's name and docstring, breaking introspection and documentation tools.
- **Incorrect stacking order** — decorators apply bottom-up, so `@auth` above `@log` means auth wraps the logged version, not the other way around.
skilldb get python-patterns-skills/DecoratorsFull skill: 168 lines
Paste into your CLAUDE.md or agent config

Decorators — Python Patterns

You are an expert in Python decorator patterns for writing idiomatic, composable, and maintainable code.

Overview

Decorators are a powerful metaprogramming feature in Python that allow you to modify or extend the behavior of functions and classes without changing their source code. They leverage Python's first-class function support and the @ syntax sugar to provide clean, reusable wrappers.

Core Philosophy

Decorators embody the open-closed principle: you can extend a function's behavior without modifying its source code. This is not just syntactic convenience — it is a design philosophy that separates cross-cutting concerns (logging, authentication, caching, retries) from business logic. When a function's core responsibility is computing a result, the retry logic and the timing instrumentation should live in decorators, not in the function body.

The power of decorators comes from Python's treatment of functions as first-class objects. A decorator is simply a function that takes a function and returns a function. This simplicity is also the discipline: a good decorator does one thing, does it transparently, and preserves the decorated function's identity (via functools.wraps). When you stack three decorators, each one should be independently understandable and testable.

Restraint matters. Decorators are a form of metaprogramming, and metaprogramming that is too clever becomes a debugging nightmare. If a decorator fundamentally changes a function's signature, return type, or error behavior in non-obvious ways, it is doing too much. The best decorators are the ones you can read in the @decorator line and immediately understand: this function retries three times, this function requires authentication, this function caches its result for five minutes.

Core Concepts

  • Function decorators accept a function and return a modified or replacement function.
  • Class decorators accept a class and return a modified or replacement class.
  • Decorator factories are callables that return decorators, enabling parameterized decoration.
  • functools.wraps preserves the original function's metadata (name, docstring, signature) on the wrapper.
  • Stacking decorators applies multiple decorators bottom-up; the outermost decorator wraps last.

Implementation Patterns

Basic function decorator

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

Decorator with arguments (decorator factory)

import functools

def retry(max_attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as exc:
                    last_exc = exc
                    print(f"Attempt {attempt} failed: {exc}")
            raise last_exc
        return wrapper
    return decorator

@retry(max_attempts=5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
    ...

Class-based decorator

import functools

class Memoize:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

@Memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

Class decorator

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

@singleton
class DatabaseConnection:
    def __init__(self, url):
        self.url = url

Decorator that works with and without arguments

import functools

def debug(func=None, *, prefix="DEBUG"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] {func.__name__} called")
            return func(*args, **kwargs)
        return wrapper

    if func is not None:
        return decorator(func)
    return decorator

@debug
def say_hello(): ...

@debug(prefix="TRACE")
def say_goodbye(): ...

Best Practices

  • Always use @functools.wraps(func) on your wrapper function to preserve the original's __name__, __doc__, and __module__.
  • Keep decorators focused on a single cross-cutting concern (logging, auth, caching, validation).
  • Prefer decorator factories when you need configuration; use the func=None sentinel pattern to support both bare and parameterized usage.
  • Use *args, **kwargs in wrappers to maintain compatibility with any function signature.
  • Type-annotate decorators with ParamSpec and TypeVar from typing for proper static analysis support.

Common Pitfalls

  • Forgetting functools.wraps causes the wrapper to shadow the original function's name and docstring, breaking introspection and documentation tools.
  • Incorrect stacking order — decorators apply bottom-up, so @auth above @log means auth wraps the logged version, not the other way around.
  • Mutable default state in decorators can leak between calls if not handled carefully; prefer instance attributes or closures scoped per decorated function.
  • Decorating methods without handling self — ensure your wrapper passes self or cls through correctly when decorating instance or class methods.
  • Breaking pickle/serialization — decorated functions may not be picklable if functools.wraps is missing or if class-based decorators are used without care.

Anti-Patterns

  • Swiss-army-knife decorator — building a single decorator that handles logging, caching, retries, and authentication all at once. This monolith is impossible to test in isolation, cannot be selectively applied, and violates the single-responsibility principle. Use one decorator per concern and stack them.

  • Hidden side effects in decoration — a decorator that modifies global state, registers the function in a global registry, and mutates the function's defaults as a side effect of importing. Decoration-time side effects make import order matter and break test isolation.

  • Signature-destroying wrappers — a decorator that changes the function's accepted parameters or return type without updating type annotations. Callers and type checkers see one signature but get different runtime behavior, leading to confusing errors.

  • Decorator that swallows exceptions — wrapping a function in a try/except that catches Exception and returns None or a default value. This hides bugs and makes debugging impossible. If a decorator handles errors, it should handle specific exceptions and make the handling visible.

  • Deep decorator stacks without documentation — stacking five or more decorators on a function without documenting the order-dependent behavior. Each decorator transforms the function, and the outermost one runs first. When the order matters (e.g., auth must run before caching), an undocumented stack is a maintenance trap.

Install this skill directly: skilldb add python-patterns-skills

Get CLI access →