Skip to main content

How to Apply Decorators to Multiple Functions in Python

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

# ✅ Apply to Function A
@my_decorator
def greet(name):
print(f"Hello, {name}!")

# ✅ Apply to Function B
@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...")

# List of functions to decorate
functions_to_track = [process_data, save_file, send_email]

# ✅ Apply decorator loop
# We must reassign the variable in the current namespace to the decorated version
process_data = logger(process_data)
save_file = logger(save_file)
send_email = logger(send_email)

# Calling them now invokes the wrapper
process_data()
save_file()

Output:

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

While you can loop through a list like for f in func_list: f = dec(f), this only modifies the local variable f inside the loop, not the function name in the global scope. You must explicitly reassign the function names as shown above or store the decorated versions in a new dictionary/list.

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):
# Overwrite the method with the decorated version
setattr(cls, attr_name, time_execution(attr_value))
return cls

# ✅ Apply to the entire class
@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

# ⛔️ Incorrect: The function now thinks it is named 'wrapper'
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):
# ✅ Correct: Preserves original function identity
@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.