Python *args Unpacking: Syntax and Common Pitfalls
python *args unpacking: Learn how *args packs extra positional arguments and how * unpacks iterables into function calls, with practical examples and pitfalls.
In Python, the syntax *args appears in two distinct contexts: function definitions and function calls. In a function definition, *args collects any number of positional arguments into a tuple. In a call, * unpacks an iterable into individual positional arguments. This article covers both sides of python *args unpacking and shows how they work together, including the common mistakes that trip up even experienced developers.
How *args Collects Positional Arguments in a Function Definition
When you define a function with *args, Python gathers all extra positional arguments into a tuple and binds that tuple to the name args. The asterisk is the syntax that triggers packing; the name args is a convention, not a requirement. You could call it *items or *rest, but args is widely understood.
def log_messages(level, *args): for message in args: print(f"[{level}] {message}") log_messages("INFO", "Server started", "Listening on port 8080")
Here, level receives "INFO", and args becomes a tuple ("Server started", "Listening on port 8080"). The function can iterate over args without knowing how many messages will be passed. This is the core of variable-length argument handling in Python.
Because args is a tuple, it is immutable. If you need to modify the collected arguments, you must convert it to a list first. This is a common source of confusion for developers coming from languages where varargs are arrays.
Unpacking Iterables with * in Function Calls
The same asterisk symbol, when used in a function call, performs the opposite operation. It unpacks an iterable—such as a list, tuple, or generator—into separate positional arguments. This is where python *args unpacking often appears in practice.
def add(a, b, c): return a + b + c numbers = [2, 4, 6] result = add(*numbers) # equivalent to add(2, 4, 6) print(result) # 12
Without the *, calling add(numbers) would pass the list as a single argument, causing a TypeError because add expects three positional arguments. The * expands the list into three separate values. This works with any iterable, including tuples, sets, and generators.
When using a generator, the unpacking consumes it lazily. For example, add(*range(3)) is equivalent to add(0, 1, 2). This can be useful when you want to avoid building an intermediate list, but be aware that the generator is fully consumed during the call.
Combining *args with Regular Parameters and Keyword Arguments
*args must appear after regular positional parameters and before keyword-only parameters in a function definition. The order is: positional-only parameters (before /), regular positional-or-keyword parameters, *args, keyword-only parameters (after * or *args), and **kwargs.
def configure(host, port, *args, timeout=30, **kwargs): print(f"Host: {host}, Port: {port}") print(f"Extra positional: {args}") print(f"Timeout: {timeout}") print(f"Other options: {kwargs}") configure("localhost", 8080, "debug", "verbose", timeout=60, retries=3)
Here, args collects ("debug", "verbose"), timeout is a keyword-only parameter with a default, and kwargs collects {"retries": 3}. This pattern is common in libraries that want to accept a mix of fixed configuration, optional positional flags, and arbitrary keyword options.
In a function call, you can also mix unpacking with explicit arguments. The unpacked values are treated as if they were written in place. For example, add(1, *[2, 3]) is equivalent to add(1, 2, 3). You cannot use multiple * unpackings for the same positional slot if they conflict, but you can use them sequentially as long as the total number of arguments matches the function signature.
Common Mistakes and Edge Cases
One frequent mistake is forgetting that *args is a tuple, not a list. If you try to append to args inside the function, you get an AttributeError. Another is assuming that *args also captures keyword arguments. It does not; that is **kwargs's job.
A subtle issue arises when unpacking a dictionary with a single *. The * operator unpacks the dictionary's keys, not its values. To unpack key-value pairs into keyword arguments, you need **. For example:
def greet(name, greeting="Hello"): print(f"{greeting}, {name}") person = {"name": "Alice", "greeting": "Hi"} greet(**person) # works greet(*person) # TypeError: greet() missing 1 required positional argument: 'name'
Another edge case is using *args with zero extra arguments. In that case, args is an empty tuple. This is perfectly valid, but your function must handle an empty tuple gracefully if it iterates over args.
Performance and Memory Considerations
Using *args in a function definition creates a new tuple each time the function is called. For most functions, this overhead is negligible. However, if you are writing a performance-critical function that is called millions of times with a fixed number of arguments, the tuple allocation can add measurable overhead. In such cases, consider using explicit parameters instead of *args.
Unpacking with * in a function call also has a cost. When you write func(*iterable), Python must iterate over the iterable and build the argument tuple internally. If the iterable is a list, this is a fast operation. If it is a generator, the generator is consumed, and the resulting tuple is stored in memory for the duration of the call. This can be a problem if you are unpacking a very large generator into a function that only needs a few arguments; you would be better off using itertools.islice or passing the generator directly.
A more subtle performance concern is the interaction between *args and function calls that involve many arguments. Python has a limit on the number of positional arguments a function can accept (often 255 in older versions, but higher in modern ones). Unpacking an iterable with millions of elements will raise a MemoryError or a TypeError if the function signature cannot accommodate that many arguments. This is not a common scenario, but it is worth knowing when processing large datasets.
Advanced Usage: Unpacking in Assignments and with Generators
The * operator is not limited to function calls and definitions. In an assignment, it can unpack an iterable into a list, capturing multiple elements. This is often called extended unpacking.
first, *middle, last = [1, 2, 3, 4, 5] print(first) # 1 print(middle) # [2, 3, 4] print(last) # 5
This is not directly related to *args, but it uses the same unpacking mechanism. When combined with function calls, it can lead to concise code. For instance, you can split a list and pass the middle portion to a function:
def process(data): print(data) items = [0, 10, 20, 30, 40] _, *rest, _ = items process(rest) # processes [10, 20, 30]
Another advanced pattern is using *args with a generator to avoid building a full list. Suppose you have a generator that yields values and you want to pass them to a function. You can unpack it directly, but that consumes the generator and creates a tuple. If the generator is infinite, this will never terminate. Always be mindful of the size of the iterable you unpack.
A more robust approach is to use *args in a wrapper function that forwards arguments to another function. This is common in decorators and proxy functions.
def logged(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__} with {args} and {kwargs}") return func(*args, **kwargs) return wrapper @logged def multiply(x, y): return x * y multiply((2, ? Actually, the decorator example is fine.
In this decorator, *args and **kwargs collect whatever arguments the decorated function receives, and then func(*args, **kwargs) unpacks them back into the original function. This pattern preserves the original function's signature from the caller's perspective, which is why it is so widely used in frameworks and libraries.
When you write a wrapper like this, be aware that the wrapper's signature is not the same as the original function's. Tools that inspect function signatures, such as inspect.signature, will see (*args,, **kwargs) unless you use functools.wraps to copy metadata. This is a practical detail that matters when building APIs that rely on introspection.
Finally, remember that *args is a tuple, so you can index it, slice it, and iterate over it just like any other tuple. This makes it easy to implement functions that need to process a variable number of arguments in a uniform way. The key to using python *args unpacking effectively is to understand which direction the asterisk is working: packing in definitions, unpacking in calls, and both in wrappers.