19.6 Forwarding Arguments with * and ** Unpacking
Forwarding arguments using the * and ** unpacking operators is a cornerstone of writing flexible and reusable Python functions. It allows a function to accept an arbitrary number of arguments and then pass them, without alteration, to another function. This pattern is fundamental to decorators, wrapper functions, and class inheritance, enabling a layer of abstraction to be inserted without interfering with the underlying function’s signature.
The mechanism works by “unpacking” a sequence (like a list or tuple) into positional arguments and a mapping (like a dict) into keyword arguments. When used in a function call, the * operator unpacks an iterable, and the ** operator unpacks a dictionary.
The Basic Unpacking Syntax
Consider a simple function that takes three arguments. You can call it by manually providing the arguments, or you can use a pre-defined list and dictionary, unpacking them into the call.
def draw_point(x, y, z):
print(f"Drawing point at coordinates: ({x}, {y}, {z})")
# Standard call
draw_point(1, 2, 3)
# Using unpacking from a list/tuple
coordinates_list = [4, 5, 6]
draw_point(*coordinates_list) # Equivalent to draw_point(4, 5, 6)
# Using unpacking from a dict (keys must match parameter names)
coordinates_dict = {'x': 7, 'y': 8, 'z': 9}
draw_point(**coordinates_dict) # Equivalent to draw_point(x=7, y=8, z=9)
# You can even mix and match
draw_point(1, *[5, 6]) # x=1, y=5, z=6
draw_point(1, z=9, **{'y': 2}) # x=1, y=2, z=9
The Power of Forwarding in Wrapper Functions
The true power of unpacking is realized in wrapper functions. A common use case is a decorator that needs to log information before and after calling the original function, but must work with any function signature.
def logging_decorator(func):
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__} with {args} and {kwargs}")
result = func(*args, **kwargs) # The critical unpacking step
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
@logging_decorator
def add_numbers(a, b):
return a + b
@logging_decorator
def say_hello(name, greeting="Hello"):
return f"{greeting}, {name}!"
# The wrapper handles both functions seamlessly
add_numbers(5, 7) # Works: passes (5, 7) as *args
say_hello("Alice") # Works: passes ("Alice",) as *args
say_hello("Bob", greeting="Hi") # Works: passes ("Bob",) and {'greeting': 'Hi'}
In this example, wrapper captures all incoming positional arguments into the args tuple and all keyword arguments into the kwargs dictionary. The magic happens on the line func(*args, **kwargs), which unpacks these collections back into individual arguments for the wrapped function func. This ensures the original function receives its arguments exactly as intended, preserving the distinction between positional and keyword arguments.
Pitfalls and Best Practices
While powerful, this technique requires careful handling to avoid subtle bugs.
Order of Unpacking Matters: The order of arguments in a function call is: positional arguments, then
*iterable, then keyword arguments, then**dictionary. You cannot have a positional argument after a**unpacked argument.Dictionary Key Mismatch: The keys in the dictionary you unpack with
**must be strings that exactly match the parameter names of the target function. A typo or an extra key will raise aTypeError.def my_func(a, b): pass bad_dict = {'a': 1, 'c': 3} # Key 'c' does not match a parameter name my_func(**bad_dict) # Raises TypeError: my_func() got an unexpected keyword argument 'c'No Double Unpacking: You cannot use multiple
*or**operators in the same call. For example,func(**d1, **d2)is invalid syntax in versions before Python 3.5. From Python 3.5 onwards, this is allowed and the dictionaries are merged, with later keys overriding earlier ones. However, it remains a common source of confusion for those on older versions.Performance Consideration for Large Data: Unpacking a very large list or tuple into arguments is functionally correct but can have a minor memory overhead, as the entire sequence must be available to be unpacked. This is rarely a practical concern but is worth noting for performance-critical code dealing with massive datasets.
Clarity is Key: Overusing unpacking, especially with complex nested structures, can make code difficult to read. It should be used purposefully, primarily when the function’s signature is dynamic or unknown (as in wrappers), not just as a shortcut for writing fewer lines of code. The goal is to write clear and maintainable code, not clever code.