Python Decorators Explained: From Simple Wrappers to Production Patterns

Python Decorators Explained: From Simple Wrappers to Production Patterns

A decorator is just a function that takes a function and returns a function. Once that clicks, everything else follows — argument decorators, class decorators, stacking, and the production patterns used in every major Python framework. This guide builds from first principles to real production patterns.

TL;DR: Always use @functools.wraps inside your wrapper to preserve metadata. Use decorator factories for configurable decorators. Stack decorators bottom-up. Common uses: timing, caching, retry, auth, validation.

What a decorator actually is

import functools

def timer(fn):
    @functools.wraps(fn)  # Preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        import time
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        ms = (time.perf_counter() - start) * 1000
        print(f"{fn.__name__} took {ms:.1f}ms")
        return result
    return wrapper

@timer  # Same as: greet = timer(greet)
def greet(name):
    return f"Hello, {name}"

greet("Alice")  # greet took 0.01ms

Decorator factories (decorators with arguments)

import functools, time

def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return fn(*args, **kwargs)
                except exceptions as e:
                    last_exc = e
                    if attempt < max_attempts:
                        time.sleep(delay * attempt)
            raise last_exc
        return wrapper
    return decorator

@retry(max_attempts=5, delay=0.5, exceptions=(ConnectionError,))
def call_api(url: str) -> dict:
    pass

# Stacking (applied bottom-up):
@timer           # Second (outer)
@retry(3, 1.0)  # First (inner)
def fetch_data(url):
    pass

Built-in caching decorators

from functools import lru_cache, cache

@cache  # Python 3.9+ unbounded
def fibonacci(n):
    if n < 2: return n
    return fibonacci(n-1) + fibonacci(n-2)

@lru_cache(maxsize=128)  # Bounded LRU
def expensive_query(user_id: int, page: int):
    pass  # Result cached per (user_id, page) combo

# Class-based decorator: singleton
def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get

@singleton
class DatabasePool:
    def __init__(self): self.connections = []

Real production decorator patterns

  • ✅ @timer — log execution time for monitoring
  • ✅ @retry(attempts=3) — retry flaky external calls
  • ✅ @lru_cache — memoize expensive pure functions
  • ✅ @require_auth — validate JWT before route handler (Flask/FastAPI)
  • ✅ @rate_limit(100, 60) — throttle function calls
  • ❌ Never forget @functools.wraps — loses function metadata
  • ❌ Never use decorators for hidden side effects callers cannot see

Decorators power the Python performance optimization — @lru_cache is one of the most impactful single-line wins. External reference: Python functools documentation.

Recommended Reading

Designing Data-Intensive Applications — The essential book every senior developer needs.

The Pragmatic Programmer — Timeless engineering wisdom for writing better code.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Don’t Miss the Next Cheat Code

Join 1,000+ senior developers getting expert JS, Python, AWS and system design secrets weekly.

✓ No spam✓ Unsubscribe anytime

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply