Python decorators are one of the language’s most elegant features. Once you understand them, you’ll use them everywhere.

What is a decorator?

A decorator is a function that takes another function as input and returns a new function — typically one that adds behaviour around the original.

python
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function runs")
        result = func(*args, **kwargs)
        print("After the function runs")
        return result
    return wrapper

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

say_hello("Alice")
# Before the function runs
# Hello, Alice!
# After the function runs

The @my_decorator syntax is just shorthand for:

python
say_hello = my_decorator(say_hello)

Why closures matter

The wrapper function is a closure — it captures func from the enclosing scope even after my_decorator has returned. This is what makes decorators possible.

Preserving the function signature

Without extra care, decorating a function loses its __name__ and __doc__. Use functools.wraps to preserve them:

python
import functools

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

Decorator with arguments

To pass arguments to a decorator, add another layer:

python
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():
    print("Hello!")

greet()  # Prints "Hello!" three times

Practical example: timing a function

python
import time
import functools

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_sum(n):
    return sum(range(n))

slow_sum(10_000_000)
# slow_sum took 0.2341s

Summary