Decorators

Difficulty: Advanced

Decorators are one of Python's most powerful features, allowing you to modify or extend the behavior of functions and methods without changing their source code. At their core, a decorator is simply a function that takes another function as an argument and returns a new function that usually extends the behavior of the original.

The @syntax is syntactic sugar that makes applying decorators clean and readable. When you write @my_decorator above a function definition, Python automatically passes that function to my_decorator() and rebinds the name to whatever my_decorator returns. This is equivalent to writing func = my_decorator(func) after the function definition.

A common problem with decorators is that the wrapper function replaces the original function's metadata (name, docstring, etc.). The functools.wraps decorator solves this by copying the original function's attributes to the wrapper. Always use @functools.wraps(func) inside your decorator to preserve introspection.

Decorators can also accept arguments. To achieve this, you need an extra layer of nesting: an outer function that accepts the decorator arguments and returns the actual decorator function, which in turn accepts and wraps the target function. This three-level pattern (decorator factory -> decorator -> wrapper) is essential for configurable decorators.

Decorators are used extensively throughout Python's ecosystem: @property, @staticmethod, @classmethod in OOP; @app.route in Flask; @login_required in Django. Understanding how to write and compose decorators is a critical skill for any advanced Python developer.

Code examples

Basic Decorator

def greet_decorator(func):
    def wrapper():
        print("Before the function call")
        func()
        print("After the function call")
    return wrapper

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

say_hello()

The @greet_decorator syntax wraps say_hello so that wrapper() runs instead, adding behavior before and after the original call.

Using functools.wraps

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, kwargs):
        print("Calling:", func.__name__)
        return func(*args, kwargs)
    return wrapper

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

print(add(3, 4))
print(add.__name__)
print(add.__doc__)

@wraps(func) preserves the original function's __name__ and __doc__ attributes, which is critical for debugging and introspection.

Decorator with Arguments

from functools import wraps

def repeat(n):
    def decorator(func):
        @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("Alice")

repeat(3) returns the actual decorator. The three-level nesting (repeat -> decorator -> wrapper) allows passing configuration to the decorator.

Stacking Multiple Decorators

from functools import wraps

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

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

@bold
@italic
def format_text(text):
    return text

print(format_text("Hello"))

Decorators are applied bottom-up: italic wraps format_text first, then bold wraps the result. This is equivalent to bold(italic(format_text)).

Key points

Concepts covered

function decorators, @syntax, functools.wraps, decorators with arguments