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

Python Decorators Explained: The Complete 2026 Guide

⏱️7 min read  ·  1,396 words


Python Decorators Explained: The Complete 2026 Guide

TechPulse Editorial Team
Tech Writers · May 24, 2026
📅 May 24, 2026⏱ 14 min read📂 Programming🏷 python · decorators · advanced-python

What Is a Python Decorator?

A Python decorator is a design pattern that lets you add new behavior to an existing function — without touching the function’s code. In Python, functions are first-class objects: they can be passed as arguments, assigned to variables, and returned from other functions. Decorators exploit this property.

The @ syntax is just syntactic sugar. When you write:

@my_decorator
def greet(name):
    return f"Hello, {name}!"

Python actually executes:

greet = my_decorator(greet)

The decorator takes the original function, wraps it in a new function with extra behavior, and returns it. The name greet now points to the enhanced version.

Your First Decorator: Step by Step

Let’s build a simple logging decorator that prints a message before and after calling any function.

def log_calls(func):
    """Decorator that logs function entry and exit."""
    def wrapper(*args, **kwargs):
        print(f"→ Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"← {func.__name__} returned {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 4)
# Output:
# → Calling add with args=(3, 4), kwargs={}
# ← add returned 7

Breaking this down:

  • log_calls receives func (the original function)
  • It defines an inner wrapper that accepts *args, **kwargs — so it works with any function signature
  • The wrapper calls the original func, captures the result, and returns it
  • log_calls returns the wrapper — not the result of calling it

The functools.wraps Fix

There’s a subtle bug in the decorator above. After decoration, add.__name__ returns 'wrapper', not 'add'. This breaks help(), inspect, and stack traces. Fix it with functools.wraps:

import functools

def log_calls(func):
    @functools.wraps(func)   # ← copies __name__, __doc__, __module__, etc.
    def wrapper(*args, **kwargs):
        print(f"→ Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"← {func.__name__} returned {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    """Add two numbers."""
    return a + b

print(add.__name__)   # 'add'  ✅
print(add.__doc__)    # 'Add two numbers.'  ✅
⚠ Rule: Always use @functools.wraps(func) inside every decorator you write. Skipping it causes mysterious bugs in logging, testing frameworks, and API documentation tools.

Decorators with Arguments

What if you want to configure your decorator? For example, @retry(times=3). You need an extra layer of nesting — a decorator factory:

import functools, time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    """Retry a function up to `times` times on specified exceptions."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_err = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_err = e
                    print(f"   Attempt {attempt}/{times} failed: {e}")
                    if attempt < times:
                        time.sleep(delay)
            raise last_err
        return wrapper
    return decorator

@retry(times=3, delay=0.5, exceptions=(ConnectionError,))
def fetch_data(url):
    # simulate a flaky network call
    raise ConnectionError("timeout")

try:
    fetch_data("https://api.example.com/data")
except ConnectionError:
    print("All retries exhausted.")

The three-level structure: retry()decorator()wrapper(). Call it: retry(times=3) returns decorator, which takes func and returns wrapper.

Stacking Multiple Decorators

You can apply multiple decorators to a single function. They apply bottom-up (innermost first):

import functools, 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

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@timer          # applied second (outer)
@log_calls      # applied first (inner)
def process(n):
    return sum(range(n))

process(1_000_000)
# Output:
# Calling process
# process took 0.0312s

Equivalent to: process = timer(log_calls(process)). Order matters — outer decorators wrap inner ones.

Class-Based Decorators

Any callable object can be a decorator. Using a class gives you state between calls:

import functools

class CallCounter:
    """Counts how many times a function is called."""
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} called {self.count} time(s)")
        return self.func(*args, **kwargs)

@CallCounter
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Alice")   # called 1 time(s)
say_hello("Bob")     # called 2 time(s)
print(say_hello.count)  # 2

@staticmethod vs @classmethod vs @property

Python ships with several built-in decorators you'll use constantly:

class Temperature:
    _kelvin_offset = 273.15

    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def fahrenheit(self):
        """Computed attribute — no () needed when accessing."""
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

    @classmethod
    def from_kelvin(cls, kelvin):
        """Factory method — creates instance from Kelvin."""
        return cls(kelvin - cls._kelvin_offset)

    @staticmethod
    def is_valid_celsius(value):
        """Utility — no access to instance or class."""
        return value >= -273.15

t = Temperature(100)
print(t.fahrenheit)                      # 212.0  (property)
t.fahrenheit = 32                        # setter
t2 = Temperature.from_kelvin(373.15)    # classmethod
print(Temperature.is_valid_celsius(-300))  # False (staticmethod)
Decorator First Arg Access Use Case
@property self Instance + class Computed attributes, getters/setters
@classmethod cls Class only Factory methods, alternate constructors
@staticmethod None Neither Utility functions namespaced to class

Real-World Use Cases

Decorators power most Python frameworks. Here are production-grade patterns:

1. Caching with LRU Cache

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))  # Instant — cached results
print(fibonacci.cache_info())  # CacheInfo(hits=48, misses=51, ...)

2. Authentication Guard (Flask/FastAPI style)

import functools

def require_auth(func):
    """Decorator for protected routes."""
    @functools.wraps(func)
    def wrapper(request, *args, **kwargs):
        token = request.headers.get("Authorization", "")
        if not token.startswith("Bearer "):
            return {"error": "Unauthorized"}, 401
        return func(request, *args, **kwargs)
    return wrapper

@require_auth
def get_user_profile(request):
    return {"user": "Alice", "email": "alice@example.com"}

3. Rate Limiter

import functools, time
from collections import defaultdict, deque

def rate_limit(calls=10, period=60):
    """Allow at most `calls` per `period` seconds per caller."""
    history = defaultdict(deque)
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            key = args[0] if args else "default"  # use first arg as caller ID
            q = history[key]
            while q and now - q[0] > period:
                q.popleft()
            if len(q) >= calls:
                raise RuntimeError(f"Rate limit exceeded: {calls} calls per {period}s")
            q.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(calls=5, period=60)
def send_email(user_id, message):
    print(f"Email sent to {user_id}: {message}")

Common Mistakes to Avoid

  • Forgetting @functools.wraps — breaks help(), tracebacks, and testing tools
  • Calling the function in the decorator bodyreturn wrapper() instead of return wrapper
  • Not using *args, **kwargs — breaks any function with parameters
  • Mutable default state shared across calls — use a closure variable, not a module-level mutable default
  • Missing return result in wrapper — silently swallows the function's return value

# ❌ Wrong — returns None
def broken(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)   # no return!
    return wrapper

# ✅ Correct
def fixed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)  # always return
    return wrapper

🚀 Level Up Your Python Skills

Now that you understand decorators, explore how they power frameworks like structuring Python projects and advanced patterns. Check our guide on Python shorthand expressions for more Pythonic code tricks.

Frequently Asked Questions

What is a Python decorator?

A Python decorator is a function that takes another function as input and returns an enhanced version — without modifying the original. The @ syntax is shorthand for func = decorator(func).

When should I use decorators?

Use decorators for cross-cutting concerns: logging, caching, auth, rate limiting, timing, input validation — any behavior you want to apply to multiple functions without repeating code.

What does functools.wraps do?

It copies the original function's __name__, __doc__, and other attributes to the wrapper, preserving identity for debugging, help(), and testing tools.

Can decorators have arguments?

Yes. Add an outer factory function that accepts the arguments and returns the real decorator. Pattern: @retry(times=3)retry() returns the decorator.

What's the difference between @staticmethod and @classmethod?

@staticmethod gets no implicit first argument — it's a plain function in a class namespace. @classmethod receives the class (cls) as the first argument, useful for factory methods.

How do stacked decorators work?

Bottom-up: the decorator closest to the def is applied first. @A @B def f() equals f = A(B(f)).

✍️ Leave a Comment

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

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