Decorators modify functions without changing their code. Here's how they work.

The Basics

A decorator is a function that takes a function and returns a function:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before")
        result = func(*args, **kwargs)
        print("After")
        return result
    return wrapper
 
@my_decorator
def say_hello():
    print("Hello!")
 
say_hello()
# Before
# Hello!
# After

The @decorator syntax is sugar for func = decorator(func).

Preserving Metadata

Use functools.wraps to preserve the original function's name and docstring:

from functools import wraps
 
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
 
@my_decorator
def greet():
    """Say hello."""
    pass
 
print(greet.__name__)  # "greet", not "wrapper"
print(greet.__doc__)   # "Say hello."

Always use @wraps.

Common Patterns

Timing

import time
from functools import wraps
 
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.2f}s")
        return result
    return wrapper
 
@timer
def slow_function():
    time.sleep(1)

Logging

def log_calls(func):
    @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

Retry

def retry(times=3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == times - 1:
                        raise
                    print(f"Retry {attempt + 1}/{times}")
        return wrapper
    return decorator
 
@retry(times=3)
def flaky_api_call():
    ...

Caching

from functools import lru_cache
 
@lru_cache(maxsize=128)
def expensive_computation(n):
    # Result cached based on arguments
    return sum(range(n))

Decorators with Arguments

def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
 
@repeat(times=3)
def say_hi():
    print("Hi!")
 
# Hi!
# Hi!
# Hi!

Three levels: factory → decorator → wrapper.

Class Decorators

Decorators can also modify classes:

def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls
 
@add_repr
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
 
print(User("Owen", 25))
# User(name='Owen', age=25)

Stacking Decorators

@decorator_a
@decorator_b
@decorator_c
def func():
    pass
 
# Equivalent to:
func = decorator_a(decorator_b(decorator_c(func)))

Order matters. Bottom decorator runs first.

Built-in Decorators

class MyClass:
    @staticmethod
    def static_method():
        # No self parameter
        pass
    
    @classmethod
    def class_method(cls):
        # Gets class, not instance
        pass
    
    @property
    def computed_value(self):
        # Access like attribute
        return self._value * 2

When to Use Decorators

Good uses:

  • Cross-cutting concerns (logging, timing, auth)
  • Caching
  • Validation
  • Registration (plugins, routes)

Avoid when:

  • Logic is too complex
  • You need to modify the function significantly
  • A regular function would be clearer

My Patterns

# Template I use
from functools import wraps
 
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Before
        result = func(*args, **kwargs)
        # After
        return result
    return wrapper

Decorators are powerful but can obscure code. Use them for clear, reusable patterns.

React to this post: