Python *args: Flexible Function Arguments Explained
python *args: Learn how Python *args collects extra positional arguments, when to use it, and how it interacts with other parameter types.
Python *args is the syntax that lets a function accept a variable number of positional arguments. When you define a function, the parameter list normally has a fixed number of positions. Adding *args changes that contract: Python collects any extra positional arguments into a tuple.
def log(message, *args): print(message) for arg in args: print(arg)
The name args is a convention, not a requirement. The asterisk is the syntax that matters. Python binds any additional positional arguments into a tuple named after the asterisk. You could call it *values or *items and the behavior would be identical.
The tuple is ordered and immutable, so you can iterate over it, index into it, or unpack it, but you cannot modify it in place.
How *args Interacts With Other Parameters
*args must appear after required positional parameters and before keyword-only parameters.
def configure(host, port, *args, timeout=30): ...
In this signature, host and port are required. Any additional positional arguments land in args. The timeout parameter is keyword-only because it appears after *args in the parameter list.
You cannot place *args before a required parameter. Python raises a SyntaxError at definition time, not at call time. This constraint keeps the parameter order predictable and prevents ambiguous call sites.
The Reverse Operation: Unpacking Arguments
The same * syntax works in the opposite direction at call time. When you call a function with *iterable, Python unpacks that iterable into individual positional arguments.
def add(a, b, c): return a + b + c values = [1, 2, 3] result = add(*values)
This is not the same as passing a list. The list is expanded into three separate arguments before the function body executes. If the list has the wrong length, Python raises a TypeError at call time.
This unpacking works with any iterable, including tuples, generators, and sets. For generators, the entire sequence is consumed during the call, which matters when the generator is infinite or extremely large.
*args vs **kwargs
*args collects positional arguments into a tuple. **kwargs collects keyword arguments into a dictionary. They solve different problems and are often used together.
| Aspect | *args | **kwargs |
|---|---|---|
| Collects | Positional arguments | Keyword arguments |
| Container | Tuple | Dictionary |
| Order preserved | Yes | Insertion order (Python 3.7+) |
| Access | Index or iterate | By key |
Use *args when the number of positional values varies and order matters. Use **kwargs when callers need to pass named options that the function forwards or merges.
A common pattern is to accept both and forward them to another function:
def wrapper(*args, **kwargs): return target(*args, **kwargs)
This is how decorators preserve the original call signature. The wrapper does not need to know what arguments the target expects.
Performance and Memory Behavior
The tuple created by *args is allocated at call time. For a function called in a tight loop with many arguments, that allocation happens on every call. In most applications this cost is negligible, but the mechanism is worth understanding.
Passing a large iterable through *args forces the entire sequence to be materialized into a tuple. If you have a generator producing millions of values and you call f(*generator), Python builds a tuple containing every value before the function starts. This can consume significant memory.
A more memory-conscious design is to accept a single iterable parameter when the function does not need individual argument positions:
def process_items(items): for item in items: ...
This avoids the intermediate tuple entirely and lets the caller pass a generator directly.
Common Mistakes and Edge Cases
One frequent mistake is forgetting that *args is a tuple, not a list. Code that attempts args.append(...) fails with an AttributeError. If you need to modify the collection, convert it first: list(args).
Another edge case is mixing *args with a trailing positional parameter. Python does not allow a required parameter after *args unless it is keyword-only. The syntax forces you to make that intent explicit:
def f(*args, flag): ...
Here flag must be passed by keyword. This is a deliberate design choice that prevents ambiguity about which arguments are positional.
Unpacking a dictionary with a single * passes its keys, not its values. To pass key-value pairs as keyword arguments, you need **.
When to Avoid *args
*args trades explicitness for flexibility. If a function accepts a small, fixed number of arguments, a normal parameter list is clearer and gives better error messages. A typo in a keyword argument name is caught immediately with named parameters; with *args and manual parsing, it may fail silently.
For public APIs where callers rely on the signature for documentation and IDE autocomplete, *args hides the contract. Prefer explicit parameters when the argument count is known at design time. Reserve *args for genuinely variable arity, such as logging, event dispatch, or decorator forwarding.
Using *args in Class Methods and Inheritance
When overriding a method, *args can help maintain compatibility when the parent signature changes. A subclass can accept *args and forward only the arguments it understands. This is common in framework code where the base class may evolve independently.
class BaseHandler: def handle(self, event, context): ... class CustomHandler(BaseHandler): def handle(self, *args, **kwargs): event = args[0] if args else kwargs.get("event") ...
This pattern is useful but should be used deliberately. It weakens the static contract of the method, and tools like type checkers will no longer be able to verify the call site against the actual parameters. If the argument set is stable, an explicit override with named parameters is safer and easier to maintain.