Skip to content

How to Apply Decorators to Multiple Functions in Python

Learn efficient techniques to apply Python decorators to multiple functions at once, including programmatic batch wrapping, class-level decorators, and preserving metadata with functools.wraps.

Python decorators allow you to extend or modify the behavior of functions without changing their source code. While the standard @decorator syntax is perfect for individual functions, applying the same logic (like logging, timing, or authentication) to dozens of functions manually violates the DRY (Don't Repeat Yourself) principle.

This guide explores efficient techniques to apply decorators to multiple functions, ranging from the standard syntax to programmatic batch application.

Method 1: Standard Individual Syntax

The most common way to apply a decorator is using the @ syntactic sugar placed immediately before the function definition. This is best when the number of functions is small.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"--- Calling {func.__name__} ---")
        return func(*args, **kwargs)
    return wrapper

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

@my_decorator
def add(a, b):
    print(f"Result: {a + b}")

greet("Alice")
add(5, 10)

Output:

--- Calling greet ---
Hello, Alice!
--- Calling add ---
Result: 15

Method 2: Programmatic Application (Loops)

If you have a list of existing functions (perhaps imported from a module) and want to wrap them all dynamically, you can apply the decorator manually in a loop. Remember that @decorator is just shorthand for func = decorator(func).

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] Executing {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def process_data():
    print("Processing data...")

def save_file():
    print("Saving file...")

def send_email():
    print("Sending email...")

functions_to_track = [process_data, save_file, send_email]

process_data = logger(process_data)
save_file = logger(save_file)
send_email = logger(send_email)

process_data()
save_file()

Output:

[LOG] Executing process_data
Processing data...
[LOG] Executing save_file
Saving file...

Method 3: Decorating All Methods in a Class

A common real-world scenario is needing to apply a decorator (e.g., authentication check) to every method within a class. Instead of adding @check to every method, you can write a class decorator that iterates over the class attributes.

def time_execution(func):
    import time
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.5f}s")
        return res
    return wrapper

def time_all_methods(cls):
    """Class decorator to apply time_execution to all methods."""
    for attr_name, attr_value in vars(cls).items():
        if callable(attr_value):
            setattr(cls, attr_name, time_execution(attr_value))
    return cls

@time_all_methods
class MathOperations:
    def slow_add(self, a, b):
        return a + b
    
    def slow_multiply(self, a, b):
        return a * b

ops = MathOperations()
ops.slow_add(10, 20)
ops.slow_multiply(5, 5)

Output:

slow_add took 0.00001s
slow_multiply took 0.00001s

Best Practice: Preserving Metadata

When you decorate multiple functions, you often lose the original function's name and docstring (they get replaced by the wrapper's name). This makes debugging difficult.

Error: Lost Metadata

def simple_decorator(func):
    def wrapper():
        """I am the wrapper"""
        return func()
    return wrapper

@simple_decorator
def my_target():
    """I am the target"""
    pass

print(f"Name: {my_target.__name__}")
print(f"Doc: {my_target.__doc__}")

Output:

Name: wrapper
Doc: I am the wrapper

Solution: functools.wraps

Always use @functools.wraps when defining a decorator. It copies the metadata from the original function to the wrapper.

import functools

def better_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@better_decorator
def my_target():
    """I am the target"""
    pass

print(f"Name: {my_target.__name__}")
print(f"Doc: {my_target.__doc__}")

Output:

Name: my_target
Doc: I am the target

Conclusion

Applying decorators allows for powerful separation of concerns (like logging, timing, and auth) from core business logic.

  1. Individual (@): Best for explicit, per-function control.
  2. Programmatic: Use when you need to apply logic to a dynamic list of functions or legacy code you cannot modify directly.
  3. Class Decorators: The most efficient way to apply a decorator to every method in a class at once.
  4. Always use functools.wraps: This ensures your debugging tools and logs show the correct function names.
Navigation