Intermediate Python / Decorators & Metaprogramming

Python Decorators: A Practical Guide

Understand how Python decorators work under the hood, build your own, and learn the patterns used in Flask, Django, and pytest.

2 min read Updated Feb 12, 2026 Reviewed Jan 20, 2026

What Is a Decorator?

A decorator is a function that takes another function as an argument, extends its behavior, and returns a new function — all without modifying the original function’s source code. The @ syntax is just syntactic sugar.

# These are equivalent:
@my_decorator
def my_function():
    pass

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

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)

slow_function()  # "slow_function took 1.0012s"

Why functools.wraps?

Without @functools.wraps(func), the wrapper function replaces the original function’s name and docstring. The wraps decorator preserves the original function’s metadata — essential for debugging and documentation.

Decorators with Arguments

To pass arguments to a decorator, you need an extra layer of nesting:

def repeat(n):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("World")  # Prints 3 times

Class-Based Decorators

class CountCalls:
    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"Call #{self.count}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()  # Call #1 n Hello!
say_hello()  # Call #2 n Hello!

Real-World Patterns

Caching / Memoization

Python’s built-in @functools.lru_cache is a decorator that caches function results:

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

Authentication (Flask-style)

def login_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if not current_user.is_authenticated:
            return redirect("/login")
        return func(*args, **kwargs)
    return wrapper

Stacking Decorators

Decorators are applied bottom-up (closest to the function first):

@decorator_a
@decorator_b
def my_func():
    pass
# Equivalent to: decorator_a(decorator_b(my_func))

Summary

Decorators are one of Python's most elegant features. They follow the open/closed principle — extending behavior without modifying existing code. Once you understand the wrapping pattern, you will see decorators everywhere in the Python ecosystem.