🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Python Decorators Complete Guide 2026: Patterns, Caching and Production Use

⏱️5 min read  ·  1,052 words

Python decorators are one of the language’s most elegant features. In 2026, decorators are everywhere — from FastAPI route definitions to dataclasses, from type checking to caching and retry logic. This complete guide takes you from the basics to building your own production-grade decorators.

What is a Decorator?

A decorator is a function that wraps another function to add behavior. The @ syntax is syntactic sugar:

# These two are equivalent:

@my_decorator
def my_function():
    pass

# Same as:
def my_function():
    pass
my_function = my_decorator(my_function)

# Simple decorator example
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Done {func.__name__}")
        return result
    return wrapper

@log_calls
def greet(name: str) -> str:
    return f"Hello, {name}!"

greet("Alice")
# Calling greet
# Done greet

functools.wraps — Preserve Metadata

import functools

def my_decorator(func):
    @functools.wraps(func)  # preserves __name__, __doc__, __annotations__
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def add(a: int, b: int) -> int:
    return a + b

print(add.__name__)   # "add" (without wraps, would be "wrapper")
print(add.__doc__)    # preserved docstring

Decorator with Arguments

import functools, time

# Decorator that accepts arguments uses three levels of nesting
def retry(max_attempts: int = 3, delay: float = 1.0, exceptions: tuple = (Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_error = e
                    if attempt < max_attempts:
                        print(f"Attempt {attempt} failed: {e}. Retrying in {delay}s...")
                        time.sleep(delay)
            raise last_error
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str) -> dict:
    import requests
    return requests.get(url, timeout=5).json()

Class-Based Decorators

import functools
import time
from collections import defaultdict

class RateLimit:
    def __init__(self, calls: int, period: float):
        self.calls = calls
        self.period = period
        self._call_times: list[float] = []

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            # Remove calls older than period
            self._call_times = [t for t in self._call_times if now - t < self.period]

            if len(self._call_times) >= self.calls:
                wait_time = self.period - (now - self._call_times[0])
                if wait_time > 0:
                    time.sleep(wait_time)

            self._call_times.append(time.time())
            return func(*args, **kwargs)
        return wrapper

@RateLimit(calls=5, period=1.0)   # max 5 calls per second
def api_request(endpoint: str) -> dict:
    import requests
    return requests.get(f"https://api.example.com{endpoint}").json()

Caching Decorators

from functools import lru_cache, cache
import time

# Simple memoization (Python 3.9+)
@cache   # unlimited cache size
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# LRU cache with size limit
@lru_cache(maxsize=128)
def expensive_computation(x: float, y: float) -> float:
    time.sleep(0.1)  # simulate slow computation
    return x ** 2 + y ** 2

# Cache with expiry (custom implementation)
def ttl_cache(ttl_seconds: float):
    def decorator(func):
        cache_store: dict = {}
        cache_times: dict = {}

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(sorted(kwargs.items())))
            now = time.time()

            if key in cache_store and (now - cache_times[key]) < ttl_seconds:
                return cache_store[key]  # return cached

            result = func(*args, **kwargs)
            cache_store[key] = result
            cache_times[key] = now
            return result
        return wrapper
    return decorator

@ttl_cache(ttl_seconds=60)
def get_exchange_rate(currency: str) -> float:
    # Fetches rate from API, cached for 60 seconds
    ...

# Inspect cache stats
print(fibonacci.cache_info())  # CacheInfo(hits=5, misses=10, maxsize=None, currsize=10)
fibonacci.cache_clear()

Decorators in FastAPI

from fastapi import FastAPI, Depends, HTTPException, Request
from functools import wraps
import time

app = FastAPI()

# FastAPI uses decorators for routing
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"id": user_id}

# Custom decorator for timing API endpoints
def measure_time(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        start = time.perf_counter()
        response = await func(*args, **kwargs)
        duration = time.perf_counter() - start
        print(f"{func.__name__}: {duration*1000:.2f}ms")
        return response
    return wrapper

@app.get("/expensive")
@measure_time
async def expensive_endpoint():
    import asyncio
    await asyncio.sleep(0.1)
    return {"status": "done"}

# Require auth decorator
def require_auth(func):
    @wraps(func)
    async def wrapper(request: Request, *args, **kwargs):
        token = request.headers.get("Authorization", "").replace("Bearer ", "")
        if not token or not validate_token(token):
            raise HTTPException(status_code=401, detail="Unauthorized")
        return await func(request, *args, **kwargs)
    return wrapper

Common Decorator Patterns

import functools, time, logging

logger = logging.getLogger(__name__)

# Timing decorator
def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        logger.debug(f"{func.__name__} took {elapsed*1000:.2f}ms")
        return result
    return wrapper

# Validation decorator
def validate_positive(func):
    @functools.wraps(func)
    def wrapper(value: float, *args, **kwargs):
        if value < 0:
            raise ValueError(f"Expected positive number, got {value}")
        return func(value, *args, **kwargs)
    return wrapper

# Singleton decorator
def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    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: str):
        self.url = url

# Deprecation warning
def deprecated(reason: str = ""):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            import warnings
            msg = f"{func.__name__} is deprecated. {reason}"
            warnings.warn(msg, DeprecationWarning, stacklevel=2)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@deprecated("Use new_function() instead")
def old_function():
    pass

Stacking Decorators

# Decorators apply bottom-to-top
@retry(max_attempts=3)
@timer
@validate_positive
def compute(value: float) -> float:
    return value ** 2

# Equivalent to:
compute = retry(max_attempts=3)(timer(validate_positive(compute)))

Python decorators are the cleanest way to add cross-cutting concerns like logging, caching, rate limiting, authentication, and retry logic without modifying the core function. Always use @functools.wraps to preserve metadata. Build parameterized decorators when you need configurable behavior.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা