DEV Community

郑沛沛
郑沛沛

Posted on

Python Decorators Demystified: From @ Syntax to Real-World Patterns

Decorators are one of Python's most powerful features, yet many developers only scratch the surface. Let's go from basics to production-ready patterns.

What's Really Happening

A decorator is just a function that takes a function and returns a function. The @ syntax is syntactic sugar:

@my_decorator
def greet():
    pass

# Is exactly the same as:
def greet():
    pass
greet = my_decorator(greet)
Enter fullscreen mode Exit fullscreen mode

Building Your First Decorator

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "done"

slow_function()  # slow_function took 1.0012s
Enter fullscreen mode Exit fullscreen mode

Key detail: always use @functools.wraps(func). Without it, your decorated function loses its __name__, __doc__, and other metadata.

Decorators with Arguments

This is where people get confused. You need a decorator factory — a function that returns a decorator:

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"Attempt {attempt} failed: {e}. Retrying in {delay}s...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=5, delay=2)
def fetch_data(url):
    # might fail due to network issues
    import urllib.request
    return urllib.request.urlopen(url).read()
Enter fullscreen mode Exit fullscreen mode

Class-Based Decorators

For complex state management, use a class:

class RateLimit:
    def __init__(self, calls_per_second=1):
        self.min_interval = 1.0 / calls_per_second
        self.last_call = 0

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - self.last_call
            if elapsed < self.min_interval:
                time.sleep(self.min_interval - elapsed)
            self.last_call = time.time()
            return func(*args, **kwargs)
        return wrapper

@RateLimit(calls_per_second=2)
def call_api(endpoint):
    print(f"Calling {endpoint}")
Enter fullscreen mode Exit fullscreen mode

Stacking Decorators

Decorators apply bottom-up:

@timer
@retry(max_attempts=3)
def process():
    pass

# Equivalent to: timer(retry(max_attempts=3)(process))
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern: Caching with Expiry

def cache(ttl_seconds=300):
    def decorator(func):
        _cache = {}

        @functools.wraps(func)
        def wrapper(*args):
            now = time.time()
            if args in _cache:
                result, timestamp = _cache[args]
                if now - timestamp < ttl_seconds:
                    return result
            result = func(*args)
            _cache[args] = (result, now)
            return result
        return wrapper
    return decorator

@cache(ttl_seconds=60)
def get_user(user_id):
    # expensive database call
    return db.query(f"SELECT * FROM users WHERE id = {user_id}")
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern: Access Control

def require_role(role):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(request, *args, **kwargs):
            if not hasattr(request, 'user') or role not in request.user.roles:
                raise PermissionError(f"Role '{role}' required")
            return func(request, *args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_user(request, user_id):
    # only admins can do this
    pass
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Always use @functools.wraps to preserve function metadata
  2. Decorator factories (decorators with arguments) need three levels of nesting
  3. Class-based decorators are great for stateful behavior
  4. Stacking order matters — bottom decorator applies first
  5. Keep decorators focused on a single concern

Decorators are the backbone of frameworks like Flask, FastAPI, and pytest. Master them and you'll understand how those frameworks work under the hood.

🚀 Level up your AI workflow! Check out my AI Developer Mega Prompt Pack — 80 battle-tested prompts for developers. $9.99

Top comments (0)