Decorate Python Functions with Arguments

Learn how to write Python decorators that work with functions accepting any number of positional and keyword arguments by forwarding them correctly.

8 min read

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:

pythonpython
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 wrapper

This 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:

pythonpython
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 wrapper

Applying 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:

pythonpython
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 wrapper

This 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:

pythonpython
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 wrapper

The 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Why do decorator wrappers need *args and **kwargs?

Decorator wrappers need *args and **kwargs because the decorator should work with functions that have any combination of positional and keyword arguments. The *args collects all positional arguments into a tuple, and **kwargs collects all keyword arguments into a dictionary. When the wrapper calls the original function by unpacking them back, it forwards everything exactly as it was received.

Can a decorator inspect or modify the arguments before forwarding them?

Yes. The wrapper receives all arguments before passing them to the original function. You can inspect positional arguments, check specific keyword values, validate types, log argument values, or even modify arguments before forwarding them. This is one of the most powerful uses of decorators: they can add validation, transformation, or logging of arguments without touching the decorated function's body.

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.