Python 3.12+ · Advanced Topic

Python Decorators
The @ Power Tool

Master Python's most elegant feature — decorators. From simple function wrappers to factory patterns, class decorators, and real‑world use cases. Write cleaner, more powerful code.

decorator_demo.py
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

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

print(slow_function())
$ python decorator_demo.py
slow_function took 1.0012s
Done!

What Are Decorators?

Functions that modify other functions — elegantly

A decorator is a function that takes another function as an argument, wraps it with additional behaviour, and returns the wrapped function. Decorators let you add logging, timing, caching, authentication, and much more — without cluttering your core logic. They are one of Python's most powerful and distinctive features.

The @ Syntax

@decorator above a function is syntactic sugar for func = decorator(func). Decorators are applied at definition time and can be stacked (applied from bottom to top).

Basics & @ Syntax

Understanding closures, wrappers, and the decorator pattern

🔧 A Simple Decorator — Without @

Before the @ sugar, a decorator is just a function that returns a wrapped version of the original. Notice the closure — the inner wrapper captures func.

def uppercase_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def greet():
    return "hello world"

greet = uppercase_decorator(greet)
print(greet())
HELLO WORLD
💡 The wrapper function is a closure — it "remembers" func even after uppercase_decorator returns. This is the foundation of all decorators.

✨ The @ Syntax — Cleaner and Readable

Using the @ symbol achieves the same result with less boilerplate. The decorator is applied when the function is defined.

def uppercase_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase_decorator
def greet():
    return "hello world"

print(greet())
HELLO WORLD

📝 Preserving Metadata — @functools.wraps

Without wraps, the decorated function loses its original name, docstring, and annotations. Always use @functools.wraps to preserve introspection.

import functools

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

@logger
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

print(add.__name__)
print(add.__doc__)
print(add(3, 4))
add
Add two numbers.
Calling add
7
💡 Without @functools.wraps, add.__name__ would show wrapper. This breaks debugging, documentation, and many frameworks (Flask, pytest, etc.).

Advanced Decorator Patterns

Factories, stacking, class decorators, and parameterized decorators

🏭 Decorator Factories — Parameterized Decorators

A decorator factory is a function that returns a decorator. This allows you to pass arguments to the decorator itself (e.g., @retry(times=3)).

import functools, time

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

@retry(times=2, delay=0.3)
def unstable():
    import random
    if random.random() < 0.5:
        raise ValueError("Random failure!")
    return "Success!"

print(unstable())
Attempt 1 failed: Random failure!. Retrying...
Success!
💡 The factory pattern is used everywhere — Flask routes (@app.route('/')), pytest fixtures (@pytest.fixture(scope='session')), and more.

📚 Stacking Decorators

You can apply multiple decorators to a single function. They are applied from bottom to top (the one closest to def runs first).

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<b>" + func(*args, **kwargs) + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<i>" + func(*args, **kwargs) + "</i>"
    return wrapper

@bold
@italic
def message():
    return "Hello"

print(message())
<b><i>Hello</i></b>
💡 Stacking order: @bold wraps @italic which wraps message(). The output is <b><i>Hello</i></b>.

🏗️ Class Decorators

Decorators work on classes too. A class decorator receives a class and returns a (possibly modified) class — great for adding attributes, methods, or registering classes.

def add_repr(cls):
    def __repr__(self):
        return f"{cls.__name__}({self.__dict__})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

p = Point(3, 4)
print(p)
Point({'x': 3, 'y': 4})

Real‑World Decorator Use Cases

What professional Python developers actually use decorators for

⏱️ Timing and Performance Monitoring

Measure execution time, log slow functions, and monitor performance in production.

import functools, time, logging

logging.basicConfig(level=logging.INFO)

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

@timed
def process_data(n: int):
    return sum(range(n))

print(process_data(1_000_000))
INFO:root:process_data took 0.0456s
499999500000

🗄️ Caching with @lru_cache

Python's built-in functools.lru_cache is a decorator that memoizes function results — essential for expensive or recursive computations.

from functools import lru_cache

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

print(fibonacci(35))
print(f"Cache info: {fibonacci.cache_info()}")
9227465
Cache info: CacheInfo(hits=33, misses=36, maxsize=128, currsize=36)
💡 @lru_cache is one of Python's most useful built-in decorators. It caches results based on arguments, dramatically speeding up recursive and repeated calls.

🔒 Authentication and Authorization

Decorators are the standard way to protect routes in web frameworks like Flask, Django, and FastAPI.

import functools

def require_admin(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_admin", False):
            raise PermissionError("Admin privileges required")
        return func(user, *args, **kwargs)
    return wrapper

@require_admin
def delete_user(user, user_id: int):
    return f"User {user_id} deleted"

admin = {"name": "Alice", "is_admin": True}
guest = {"name": "Bob", "is_admin": False}

print(delete_user(admin, 42))
try:
    print(delete_user(guest, 42))
except PermissionError as e:
    print(f"Error: {e}")
User 42 deleted
Error: Admin privileges required
💡 This pattern is exactly how @login_required in Flask, @permission_required in Django, and @Depends in FastAPI work under the hood.

Decorator FAQ

Quick answers to the most common decorator questions

What is the difference between a decorator and a decorator factory?

+

A decorator is a function that takes a function and returns a function. A decorator factory is a function that returns a decorator — it allows you to pass parameters to the decorator (e.g., @retry(times=3)).

Why do I need @functools.wraps?

+

Without wraps, the decorated function loses its original name (__name__), docstring (__doc__), annotations, and module. This breaks debugging, documentation tools, and frameworks. Always use @functools.wraps.

Can decorators be stacked?

+

Yes. Multiple decorators are applied from bottom to top (closest to def runs first). For example, @bold @italic def message() applies italic first, then bold.

Do decorators work on methods and classes?

+

Yes. Decorators work on any callable — functions, methods, classes, and even generator functions. Class decorators receive a class and can modify or replace it.

What are some common built‑in decorators?

+

@staticmethod, @classmethod, @property, @functools.lru_cache, @dataclass, and @contextlib.contextmanager. Also @wraps (for writing decorators) and @total_ordering.

Back to Home