Skip to main content

Decorators (Part 2): Decorators with Arguments

In our last article, we learned that a decorator is a function that wraps another function to add new behavior. However, our first decorator was simple: it only worked on functions that took no arguments.

What happens if we try to use our logger_decorator on a function that takes a parameter?

def logger_decorator(func_to_wrap):
def wrapper():
print("Logging...")
func_to_wrap()
return wrapper

@logger_decorator
def greet(name): # This function takes an argument
print(f"Hello, {name}!")

# This will crash!
# greet("Alice")

This would fail with a TypeError, because our wrapper() function doesn't accept any arguments, but we tried to call greet with one ("Alice"). To fix this, we need to make our wrapper function capable of accepting any arguments.


📚 Prerequisites

You should understand the basic decorator pattern and have a good grasp of *args and **kwargs for accepting a variable number of arguments.


🎯 Article Outline: What You'll Master

In this article, you will learn:

  • Generic Decorators: How to use *args and **kwargs to create a decorator that can wrap any function, regardless of its parameters.
  • Handling Return Values: How to correctly capture and return the result of the original function from within the decorator.
  • The Problem with Metadata: See how decorators can obscure a function's original identity (__name__, __doc__).
  • functools.wraps: The standard solution for preserving the original function's metadata.

🧠 Section 1: Accepting Arguments with *args and **kwargs

To make our decorator universal, we need its inner wrapper function to accept any possible combination of positional and keyword arguments. The perfect tool for this is the *args and **kwargs syntax.

The wrapper function will collect all positional arguments into a tuple called args and all keyword arguments into a dictionary called kwargs. It can then pass them along when it calls the original function.

Let's fix our logger_decorator.

# generic_decorator.py

def logger_decorator(func_to_wrap):
# The wrapper now accepts any arguments
def wrapper(*args, **kwargs):
print(f"Calling function '{func_to_wrap.__name__}' with arguments:")
print(f" Positional (args): {args}")
print(f" Keyword (kwargs): {kwargs}")

# Pass the collected arguments to the original function
func_to_wrap(*args, **kwargs)

return wrapper

@logger_decorator
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")

# Now it works!
greet("Alice")
greet("Bob", greeting="Good morning")

Output:

Calling function 'greet' with arguments:
Positional (args): ('Alice',)
Keyword (kwargs): {}
Hello, Alice!
Calling function 'greet' with arguments:
Positional (args): ('Bob',)
Keyword (kwargs): {'greeting': 'Good morning'}
Good morning, Bob!

This single pattern (def wrapper(*args, **kwargs): ... func_to_wrap(*args, **kwargs)) makes your decorator incredibly robust and capable of wrapping almost any function.


💻 Section 2: Handling Return Values

What if the decorated function returns a value? Our current decorator doesn't capture it. The wrapper function needs to return the result of the original function call.

Let's create a simple timing decorator.

import time

def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
# Capture the result of the original function call
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function '{func.__name__}' took {end_time - start_time:.4f} seconds to run.")
# Return the result to the original caller
return result
return wrapper

@timer_decorator
def add(x, y):
"""A simple function that adds two numbers."""
time.sleep(1) # Simulate work
return x + y

# Call the function and get its return value
sum_result = add(10, 20)
print(f"The result of the add function is: {sum_result}")

Output:

Function 'add' took 1.0012 seconds to run.
The result of the add function is: 30

🛠️ Section 3: Preserving Function Metadata with functools.wraps

Decorators have one subtle side effect: they replace the original function with the wrapper function. This means the original function's metadata is lost.

# Let's inspect our 'add' function from the previous example
print(f"Function name: {add.__name__}")
print(f"Docstring: {add.__doc__}")

Output:

Function name: wrapper
Docstring: None

This is a problem for debugging and introspection tools. The function is no longer called add, and its helpful docstring is gone.

The solution is to use the @wraps decorator from Python's built-in functools module. You apply it to your wrapper function, and it copies the metadata from the original function (func_to_wrap) to the wrapper.

The Final, Correct Decorator Pattern:

import time
import functools # Import the module

def timer_decorator(func):
# Apply the wraps decorator to the wrapper function
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function '{func.__name__}' took {end_time - start_time:.4f} seconds to run.")
return result
return wrapper

@timer_decorator
def add(x, y):
"""A simple function that adds two numbers."""
time.sleep(1)
return x + y

# Now let's inspect it again
print(f"Function name: {add.__name__}")
print(f"Docstring: {add.__doc__}")

Output:

Function name: add
Docstring: A simple function that adds two numbers.

Perfect! By using @functools.wraps, we get all the benefits of the decorator without losing the identity of our original function.


✨ Conclusion & Key Takeaways

You now have the complete, robust pattern for writing decorators that can be applied to any function. This pattern is the foundation for many of the powerful features you'll see in Python frameworks.

Let's summarize the key takeaways:

  • Use *args and **kwargs in your wrapper function to accept any arguments.
  • Pass *args and **kwargs when you call the original function inside the wrapper.
  • Remember to return the result of the original function call from your wrapper.
  • Always use @functools.wraps on your inner wrapper function to preserve the original function's metadata.

Challenge Yourself: Create a decorator called debug_info that prints the function name, its arguments (args and kwargs), and its return value every time the decorated function is called.


➡️ Next Steps

We've seen how to create decorators that wrap functions. But what if you want to create a decorator that can be customized with its own arguments? In the next article, we'll explore "The functools Module: wraps and lru_cache," diving deeper into wraps and another incredibly useful decorator.

Happy coding!