Decorators
Decorator patterns for wrapping, extending, and composing Python functions and classes
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 linesDecorators — 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.wrapspreserves 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=Nonesentinel pattern to support both bare and parameterized usage. - Use
*args, **kwargsin wrappers to maintain compatibility with any function signature. - Type-annotate decorators with
ParamSpecandTypeVarfromtypingfor proper static analysis support.
Common Pitfalls
- Forgetting
functools.wrapscauses the wrapper to shadow the original function's name and docstring, breaking introspection and documentation tools. - Incorrect stacking order — decorators apply bottom-up, so
@authabove@logmeans 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 passesselforclsthrough correctly when decorating instance or class methods. - Breaking pickle/serialization — decorated functions may not be picklable if
functools.wrapsis 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
Exceptionand returnsNoneor 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
Related Skills
Async Patterns
Asyncio patterns for concurrent I/O-bound programming in Python
Context Managers
Context manager patterns using with statements for reliable resource management in Python
Dataclasses
Dataclass and Pydantic model patterns for structured data in Python
Dependency Injection
Dependency injection patterns for loosely coupled, testable Python applications
Generators
Generator and itertools patterns for memory-efficient data processing in Python
Metaclasses
Metaclass and descriptor patterns for advanced class customization in Python