Python *args and **kwargs: Variable-Length Arguments Without the Confusion
Most function definitions list a fixed number of parameters. But sometimes you do not know in advance how many arguments a caller will provide. Python handles this with *args for variable-length positional arguments and **kwargs for variable-length keyword arguments.
*args: Collecting Extra Positional Arguments
When you prefix a parameter with *, Python collects all positional arguments beyond the explicitly named ones into a tuple bound to that parameter name.
def total(*numbers): return sum(numbers)
print(total(1, 2, 3)) # 6print(total(10, 20, 30, 40)) # 100print(total()) # 0 β empty tuple, sum returns 0Inside the function, numbers is a regular tuple. You can iterate over it, pass it to sum(), slice it, or check its length.
The name args is conventional, not required. *values, *items, or any other name works just as well. The * is what makes it collect.
def first_and_rest(first, *rest): print(f"First: {first}") print(f"Rest: {rest}")
first_and_rest("a", "b", "c", "d")# First: a# Rest: ('b', 'c', 'd')The first positional argument goes to first as normal. Everything else goes into rest.
**kwargs: Collecting Extra Keyword Arguments
** collects all keyword arguments that were not matched by named parameters into a dict.
def show_info(**details): for key, value in details.items(): print(f"{key}: {value}")
show_info(name="Alice", age=30, city="London")# name: Alice# age: 30# city: LondonThe caller passes name="Alice" and age=30 β both are keyword arguments. Inside the function, details is {"name": "Alice", "age": 30, "city": "London"}.
Using Both Together
You can combine *args and **kwargs in the same function. The order matters: positional parameters first, then *args, then named keyword parameters (with or without defaults), then **kwargs.
def create_tag(tag_name, *children, class_name=None, **attributes): attrs = "" if class_name: attrs += f' class="{class_name}"' for key, value in attributes.items(): attrs += f' {key}="{value}"'
inner = "".join(str(c) for c in children) return f"<{tag_name}{attrs}>{inner}</{tag_name}>"
print(create_tag("p", "Hello, world!"))# <p>Hello, world!</p>
print(create_tag("a", "Click here", class_name="link", href="/about", target="_blank"))# <a class="link" href="/about" target="_blank">Click here</a>tag_name is a required positional argument. *children collects any additional positional arguments (the text or nested tags). class_name is a keyword-only parameter with a default. **attributes collects any other keyword arguments as HTML attributes.
Argument Order Rules
Python enforces a strict ordering for function parameters:
- Regular positional parameters (
a, b, c) *args(or bare*for keyword-only without collecting)- Keyword-only parameters (after
*) **kwargs
# Validdef func(a, b, *args, keyword_only, **kwargs): pass
# Invalid β **kwargs must come lastdef bad(a, **kwargs, b): # SyntaxError passUnpacking When Calling
The same * and ** syntax also works at the call site to unpack sequences into positional arguments and dictionaries into keyword arguments.
def add(a, b, c): return a + b + c
values = [1, 2, 3]print(add(*values)) # 6 β equivalent to add(1, 2, 3)
settings = {"a": 10, "b": 20, "c": 30}print(add(**settings)) # 60 β equivalent to add(a=10, b=20, c=30)This is particularly useful when you have data in a list or dict and want to pass it to a function that expects individual arguments.
Real Use Case: Decorators and Wrappers
Decorators need to forward all arguments to the wrapped function without knowing what those arguments are. *args and **kwargs make this possible.
import timeimport functools
def timed(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) # forward everything elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper
@timeddef slow_operation(n, multiplier=1): return sum(range(n)) * multiplier
print(slow_operation(1_000_000))print(slow_operation(500_000, multiplier=2))wrapper does not need to know the signature of func. It collects all arguments and forwards them intact.
Real Use Case: Extending a Parent Class Constructor
class LoggedDict(dict): def __init__(self, *args, log_level="INFO", **kwargs): super().__init__(*args, **kwargs) # forward everything to dict self.log_level = log_level print(f"[{log_level}] LoggedDict created with {len(self)} items")
d = LoggedDict({"a": 1, "b": 2}, log_level="DEBUG")# [DEBUG] LoggedDict created with 2 itemsprint(d["a"]) # 1The *args and **kwargs let LoggedDict.__init__ accept everything that dict.__init__ accepts, while still intercepting log_level for its own use.
Common Mistakes
Wrong parameter order. Placing **kwargs before *args or before keyword-only parameters is a SyntaxError.
Assuming args is a list. It is a tuple. If you need to modify it, convert: args = list(args).
Confusing the definition with the call syntax. In the function signature, *args collects. At the call site, *my_list unpacks. Both use *, but they do opposite things.
Overusing **kwargs to avoid defining parameters. If a function needs specific keyword arguments, name them explicitly. Using **kwargs for everything hides the functionβs interface and makes it harder to use and test.