Python *args Explained

Learn how *args lets Python functions accept any number of positional arguments, how it works under the hood as a tuple, and when to use it in your own functions.

7 min read

Python's *args syntax, pronounced "star args," is the mechanism that lets a function accept any number of positional arguments without listing each one as a separate named parameter. You have already used functions that rely on *args without realizing it: the built-in print function accepts any number of values to display, and the max and min functions accept any number of values to compare. These functions would be impractical to write with fixed parameter lists because you never know in advance how many values a caller will want to print or compare. The *args pattern solves this problem elegantly and is one of the features that makes Python feel productive for data processing and scripting tasks.

The asterisk in *args is the unpacking operator, and args is the conventional name for the tuple that collects the extra arguments. You can use any valid variable name after the asterisk, such as *numbers or *items, but args is so universally recognized that deviating from it will cause every Python developer who reads your code to pause and wonder why. This article covers how *args works in function definitions, how the same asterisk syntax works differently at call sites to unpack collections, and the practical patterns that make *args an essential tool in your function-writing toolkit.

How *args collects extra positional arguments

When you place *args in a function's parameter list, Python takes any positional arguments that were not assigned to named parameters and packs them into a tuple. The function body can then iterate over that tuple, index into it, or pass it along to another function. The simplest demonstration shows that *args captures arguments that would otherwise cause a TypeError for too many arguments:

pythonpython
def log_all(*args):
    for item in args:
        print(f"LOG: {item}")
 
log_all("startup", "connected", "ready")

The three strings are collected into a tuple named args, and the for loop prints each one with a prefix. Call log_all with no arguments and the loop simply does not run because the args tuple is empty. Call it with ten arguments and all ten are processed the same way. This uniform handling of zero through many arguments is exactly what makes print and max work without fixed parameter counts.

The args tuple supports all standard tuple operations. You can check its length with len(args), access specific elements by index like args[0], slice it like args[1:], and unpack it into variables if you know how many arguments to expect. The tuple is immutable, which means the function cannot accidentally modify the caller's data, a guarantee that makes *args safe to use in functions that pass their arguments through to other functions.

Where *args fits in the parameter list

The position of *args in a function signature follows strict rules. Required parameters come before *args, and any additional keyword-only parameters come after it. Parameters with default values cannot appear between the required parameters and *args because *args consumes all remaining positional arguments, leaving nothing for a defaulted parameter to receive positionally:

pythonpython
def configure(name, *args, debug=False):
    print(f"Configuring {name}")
    print(f"Extra options: {args}")
    if debug:
        print("Debug mode enabled")
 
configure("server", "verbose", "cached", debug=True)

The string "server" fills the required name parameter. The strings "verbose" and "cached" go into the args tuple. The debug parameter is keyword-only because it appears after *args, so it must be passed as debug=True rather than as a fourth positional argument. This pattern of required parameters, then *args for extras, then keyword-only parameters for options appears in many real Python libraries because it keeps the function signature clear: required inputs are named, optional extras are collected without ceremony, and configuration flags are labeled.

Using *args to forward arguments to another function

One of the most common uses of *args is in wrapper functions that receive arguments and pass them through to another function without inspecting or modifying them. This pattern appears in decorators, subclass methods, and any situation where you want to add behavior before or after calling an existing function without changing how that function is called:

pythonpython
def timed(func):
    def wrapper(*args):
        import time
        start = time.time()
        result = func(*args)
        print(f"Took {time.time() - start:.3f}s")
        return result
    return wrapper

The wrapper function accepts *args to capture any number of positional arguments, then uses func(*args) to forward them to the original function. The wrapper does not need to know what parameters func expects or how many there are; *args handles zero through many arguments transparently. This pattern is explored in depth in the article on Python decorators explained later in this learning path, but the core mechanism is the same *args syntax used for both collecting and forwarding. Understanding this forwarding pattern also requires familiarity with how to define and call Python functions, since the wrapper and the wrapped function both follow the same definition and calling conventions.

This forwarding pattern has an important property: it preserves the calling convention. If func expects two positional arguments, the wrapper also accepts two positional arguments and passes them through unchanged. The caller of the wrapper does not need to know that an intermediate function exists because the argument handling is transparent. This transparency is what makes decorators and wrappers composable, you can stack multiple wrappers around a function and each one only needs to forward *args without understanding the full parameter list.

The asterisk operator at the call site

The asterisk has a second role in Python that is the mirror image of its role in function definitions. When you use * before an iterable in a function call, Python unpacks that iterable into individual positional arguments. While *args in a definition packs arguments into a tuple, *items in a call unpacks a tuple or list into arguments:

pythonpython
values = [10, 20, 30]
print(*values)  # Same as print(10, 20, 30)

The list values is unpacked into three separate arguments for the print function. Without the asterisk, calling print(values) would print the list as a single object with square brackets, which is rarely what you want. The unpacking operator works with any iterable: lists, tuples, sets, and even generator expressions. This is a different operation from the *args in definitions, but it uses the same syntax, and understanding both roles of the asterisk is essential for reading real Python code.

Combining *args in definitions with * unpacking in calls creates a powerful pattern for building flexible data pipelines. A function that receives *args can store arguments, transform them, and pass them to another function using the * unpacking operator. The function in the middle does not need to know how many arguments are flowing through or what their types are; it only needs to treat them as a collection and forward them correctly.

When to use *args in your own functions

Use *args when your function genuinely needs to handle a variable number of inputs and listing every possible parameter name would be impractical or impossible. The built-in print function is the classic example: you cannot predict how many values a caller will want to print, so *args is the correct solution. Other good use cases include functions that aggregate values, such as a function that returns the product of all its arguments, and functions that log or record events, where each call might include a different number of details.

Do not use *args as a way to avoid thinking about your function's parameter list. If a function always needs exactly two inputs, write two named parameters. Named parameters are self-documenting, appear in IDE autocomplete, and give Python the opportunity to catch errors where the wrong number of arguments is passed. Using *args for a function that should take exactly two arguments hides the function's expectations and delays error detection from definition time to runtime, where it is harder to debug.

A practical middle ground is to accept required named parameters for the essential inputs and use *args for optional additional values. A function that validates an email address might take the address itself as a required parameter and use *args for additional validation rules that vary by use case. This keeps the primary input visible in the signature while allowing flexibility for the secondary inputs that are genuinely variable in number and meaning.

Rune AI

Rune AI

Key Insights

  • *args in a function definition collects extra positional arguments into a tuple named args.
  • The name args is a convention; any valid identifier works, but sticking with args keeps your code readable.
  • Required parameters must come before *args; keyword-only parameters must come after it.
  • The * unpacking operator at a call site spreads a list or tuple into individual positional arguments.
  • Iterate over args with a for loop to process a variable number of inputs in a uniform way.
RunePowered by Rune AI

Frequently Asked Questions

What does *args do in a Python function definition?

The *args syntax collects any extra positional arguments passed to a function into a tuple named args. The asterisk is the unpacking operator, and args is the conventional name, though you can use any valid variable name after the asterisk. This lets a function accept a variable number of positional arguments without listing them explicitly as separate parameters.

Can I use a different name instead of args?

Yes. The name args is a strong convention but not a language requirement. You can write *numbers, *items, or any valid identifier after the asterisk. The convention is so widespread that deviating from it will surprise other Python developers, so stick with *args unless you have a specific reason to choose a different name.

Can a function have both regular parameters and *args?

Yes. Required parameters come first, then *args, then keyword-only parameters after it. When calling the function, regular positional arguments fill the required parameters first, any remaining positional arguments go into the args tuple, and keyword arguments after that target keyword-only parameters. The order is: required, *args, keyword-only.

Conclusion

*args is Python's mechanism for writing functions that handle a flexible number of positional arguments. It collects extra arguments into a tuple, integrates cleanly with regular parameters and default values, and enables the unpacking pattern that spreads a collection into individual arguments at the call site. Mastering *args is the first step toward writing truly flexible Python function interfaces.