Decorating a function that takes no arguments is a straightforward exercise. But real functions take arguments, often many of them, so learning how to decorate Python functions with arguments is essential for building reusable wrappers. A decorator that only works with zero-argument functions is barely useful. A decorator that works with any function, regardless of its signature, requires the wrapper to accept and forward arguments generically. This article explains how to write decorators that handle arguments correctly, using the standard mechanism for capturing and forwarding arbitrary function arguments.
The challenge is that a decorator author does not know in advance what kind of function the decorator will be applied to. You might write a timing decorator today for a function that takes two integers, but next week you might apply the same decorator to a function that takes a string, a list, and three keyword arguments. The wrapper must accept whatever the decorated function expects, pass it through without distortion, and return whatever the decorated function returns. The solution is to define the wrapper with star-args and double-star-kwargs, which together capture every possible argument the caller could pass, and then forward them to the original function using the same unpacking syntax in the call.
What makes argument forwarding worth a dedicated article is the range of things a decorator can do with arguments beyond simply passing them along. A decorator can inspect arguments before the original function runs, which enables input validation that rejects invalid data before the function body executes. A decorator can log arguments for debugging, which helps trace data flow through a system without adding print statements to every function. A decorator can modify arguments, transforming or normalizing them before the original function sees them. And a decorator can even suppress the original function call entirely under certain conditions, which is how caching and memoization decorators avoid redundant work. Each of these capabilities depends on the wrapper having full access to the incoming arguments.
The args and kwargs forwarding pattern
When a caller invokes a decorated function, Python actually calls the wrapper. The wrapper's parameter list determines what arguments it can receive. If the wrapper is defined with star-args and double-star-kwargs, it accepts everything. The star collects all positional arguments into a tuple, and the double-star collects all keyword arguments into a dictionary. Neither the wrapper nor the decorator needs to know the names or types of these arguments in advance.
Inside the wrapper, you call the original function by unpacking the tuple and dictionary back into individual arguments. If you want a refresher on how the packing and unpacking mechanics work outside decorators, the articles on Python *args explained and Python **kwargs explained cover them in isolation. The original function receives exactly the arguments the caller passed, in exactly the same form. Here is the pattern applied to the timing decorator from the article on creating your first Python decorator:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} ran in {end - start:.4f}s")
return result
return wrapperThis single decorator works with a function that takes no arguments, a function that takes five positional arguments, a function that takes keyword arguments only, and a function that takes a mix of both. You can verify this by applying the timer to functions with different signatures and confirming that each one runs and reports its timing correctly. The wrapper neither knows nor cares what arguments pass through it, which is exactly the property that makes the decorator reusable.
If the wrapper omitted the double-star pattern and only used the star for positional arguments, it would fail when the decorated function was called with a keyword argument. The call would raise a TypeError because the wrapper does not accept keyword arguments. If the wrapper omitted the star and only used the double-star, it would fail when the decorated function was called with a positional argument. Using both together is the safe default for any decorator that is meant to be applied broadly.
Inspecting arguments inside the wrapper
Because the wrapper receives the arguments before the original function does, it can inspect them for logging, validation, or debugging purposes. A common use case is a decorator that logs function calls with their arguments, which is invaluable for tracing execution in a complex system. Here is a decorator that prints the function name and all arguments every time the decorated function is called:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapperApplying this log-calls decorator to any function produces a trace of every invocation, including the arguments passed and the value returned. This decorator is simple enough to be harmless in development, but it demonstrates the pattern that more sophisticated decorators follow. A production version might send the same information to a logging framework, filter out sensitive arguments like passwords, or sample only a fraction of calls to avoid overwhelming the log output.
Argument inspection also enables validation decorators. A decorator can check that arguments meet certain criteria before allowing the original function to run. If a function expects a positive integer but receives a negative one, the decorator can raise a ValueError immediately, with a clear message, rather than letting the function produce a confusing error later. Here is a decorator that ensures numeric arguments are positive:
def require_positive(func):
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(f"Argument {arg} must be positive")
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(f"Argument {key}={value} must be positive")
return func(*args, **kwargs)
return wrapperThis decorator iterates over positional arguments in the args tuple and keyword arguments in the kwargs dictionary, checks whether each numeric argument is positive, and raises a ValueError if any is not. The original function only runs if all numeric arguments pass the check. This pattern of pre-validation keeps validation logic out of individual functions and makes it easy to apply the same rules consistently across an entire codebase.
Modifying arguments before forwarding
Beyond inspection, a decorator can modify arguments before the original function receives them. This is useful for normalization, where you want to ensure that arguments are in a canonical form regardless of what the caller passes. For example, a decorator that lowercases all string arguments before passing them to a function that performs case-sensitive matching:
def lowercase_args(func):
def wrapper(*args, **kwargs):
new_args = tuple(
arg.lower() if isinstance(arg, str) else arg
for arg in args
)
new_kwargs = {
k: v.lower() if isinstance(v, str) else v
for k, v in kwargs.items()
}
return func(*new_args, **new_kwargs)
return wrapperThe wrapper creates modified copies of args and kwargs where every string value has been lowercased. The original tuple and dictionary are not mutated, which is important because tuples are immutable and mutating the caller's dictionary could cause surprising side effects. The modified copies are unpacked into the call to the original function, which sees only the normalized values.
Argument modification decorators should be documented clearly because they change the contract between the caller and the function. A caller who passes a mixed-case string to a function decorated with the lowercase-args decorator might be surprised if the function behaves as though it received a lowercase version. When the modification is intentional and documented, as with normalization decorators, this is acceptable and often desirable. When the modification is unexpected, it creates bugs that are difficult to trace because the transformation is invisible at the call site.
Handling functions with default arguments
Functions with default arguments work naturally with the forwarding pattern because default values are applied by Python before the wrapper receives the arguments. If a function is defined with a greeting parameter that defaults to "Hello" and the caller provides only a name, Python resolves the missing greeting to its default value before the call reaches the wrapper. The wrapper sees two positional arguments even though the caller only provided one.
This means a decorator that inspects arguments sees the resolved values, including defaults, which is usually the desired behaviour. If you are logging function calls for debugging, you want to see the actual arguments the function will receive, not the partial list the caller provided. The downside is that the decorator cannot distinguish between an argument the caller passed explicitly and one that Python filled in from a default, but for most decorator use cases this distinction does not matter.
Functions with keyword-only arguments, defined after a bare star in the parameter list, also work transparently with the forwarding pattern. If a function is defined with host as a positional argument and port and timeout as keyword-only arguments, the caller can pass only the positional argument and one keyword argument. The wrapper receives a single-element args tuple and a kwargs dictionary with one entry. The port argument, which the caller did not provide, does not appear in kwargs because Python resolves defaults at the call boundary before the wrapper receives anything.
What happens when forwarding goes wrong
The most common mistake when writing decorators that handle arguments is to forget the double-star pattern in either the wrapper definition or the forwarding call. If the wrapper is defined with only star-args and no double-star, any call that includes a keyword argument will fail with an unexpected keyword argument TypeError. The error message points at the wrapper definition, not at the decorator, which can be confusing if you do not remember that the wrapper is now the function being called.
Another common mistake is to accidentally consume or drop arguments. If the wrapper calls the original function with no arguments instead of unpacking args and kwargs, the original function receives nothing regardless of what the caller passed. If the function expects arguments, it will raise a TypeError about missing arguments, and the stack trace will point inside the wrapper, not at the call site. This is another reason to test decorators with functions that take arguments of different kinds before applying them broadly.
A subtler issue arises when the wrapper modifies args or kwargs in place instead of creating copies. Tuples are immutable, so args cannot be modified in place, but kwargs is a regular dictionary. If the wrapper adds an extra key to kwargs before calling the original function, the caller's dictionary is mutated, and any code that iterates over keyword arguments after the call will see the extra key. The safe approach is to build new tuples and dictionaries rather than modifying the ones the wrapper received.
Once you are comfortable forwarding arguments through decorators, the next article addresses the other side of the call: ensuring that return values from Python decorators are preserved correctly, so the decorated function's output reaches the caller exactly as if the decorator were not there.
Rune AI
Key Insights
- Use *args and **kwargs in every decorator wrapper so the decorator works with functions of any signature.
- The wrapper receives arguments, optionally inspects or modifies them, forwards them to the original function, and returns the result.
- A decorator can validate argument types, log argument values, or transform arguments before the original function sees them.
- Decorators that modify arguments should document the modification clearly, as callers may not expect their arguments to change.
- The same *args and **kwargs pattern works for functions with default arguments, keyword-only arguments, and positional-only arguments.
Frequently Asked Questions
Why do decorator wrappers need *args and **kwargs?
Can a decorator inspect or modify the arguments before forwarding them?
Conclusion
Forwarding arguments through a decorator wrapper is the detail that separates a fragile decorator from a robust one. The star-args and double-star-kwargs pattern, combined with the discipline of always forwarding both, ensures that your decorators work with any function you apply them to, regardless of how many arguments that function accepts or how those arguments are passed. This pattern is so fundamental that nearly every decorator in the Python standard library and third-party ecosystem uses it.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.