*args and **kwargs in Python
*args collects any extra positional arguments into a tuple, and **kwargs collects any extra keyword arguments into a dict. They let a function accept a variable number of arguments โ the names args and kwargs are just convention; the * and ** do the work.
What Does *args Do?
Inside the function, args is a plain tuple holding every positional argument beyond the named parameters, so the function works with 0, 1, or 50 values.
def total(*args):
return sum(args)
total(1, 2) # 3
total(1, 2, 3, 4) # 10
total() # 0 โ args is an empty tuple
What Does **kwargs Do?
kwargs is a dict mapping each extra keyword argument's name to its value, preserving the order they were passed in.
def build_url(base, **kwargs):
query = "&".join(f"{k}={v}" for k, v in kwargs.items())
return f"{base}?{query}" if query else base
build_url("/search", q="pandas", page=2)
# '/search?q=pandas&page=2'
What Is the Correct Parameter Order?
Parameters must appear in this order: regular positional, *args, keyword-only parameters, then **kwargs. Anything defined after *args can only be passed by keyword.
def f(a, b=2, *args, mode="fast", **kwargs):
...
f(1, 2, 3, 4, mode="slow", debug=True)
# a=1, b=2, args=(3, 4), mode="slow", kwargs={"debug": True}
# def g(**kwargs, *args): <- SyntaxError, ** must come last
How Does Unpacking with * and ** Work?
The same symbols work in reverse at the call site: * spreads a sequence into positional arguments and ** spreads a dict into keyword arguments.
coords = (3, 7)
opts = {"mode": "slow", "debug": True}
f(*coords, **opts) # f(3, 7, mode="slow", debug=True)
# Also handy for merging dicts:
defaults = {"timeout": 30, "retries": 3}
config = {**defaults, "retries": 5} # {'timeout': 30, 'retries': 5}
How Do You Forward Arguments to a Wrapped Function?
The classic use is a wrapper that passes everything through untouched: accept *args, **kwargs, forward them with *args, **kwargs. This is the backbone of every decorator โ see Python decorators for the full pattern.
import functools, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # forward everything
print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
return result
return wrapper
Common Pitfalls
- args is a tuple, not a list: you can't append to it. Convert with
list(args)if needed. - Passing a list without *:
total([1, 2, 3])gives args =([1, 2, 3],)โ one element. You wantedtotal(*[1, 2, 3]). - Duplicate argument errors:
f(1, a=1)raisesTypeError: got multiple values for argument 'a'. - Swallowing typos: a function taking
**kwargswon't complain abouttimout=30. Prefer explicit parameters in public APIs.
Pro Tip: A bare * in the signature forces keyword-only arguments without accepting extras: def resize(image, *, width, height) makes resize(img, 800, 600) an error, so nobody swaps width and height by accident.