PYTHON / FUNCTIONS
*args and **kwargs
Write functions that accept any number of positional and keyword arguments with *args and **kwargs, and spread sequences and dicts back out at the call site.
What you will learn
- Read *args as "the rest of the positional arguments, as a tuple"
- **kwargs is a fresh dict of keyword arguments no named parameter claimed
- Order parameters: named, then *args, then keyword-only, then **kwargs last
- Forward a call unchanged with helper(*args, **kwargs) in wrappers and decorators
Understanding *args and **kwargs
In a `def`, a single star before a parameter name tells Python: bind every positional argument that no earlier parameter claimed into a tuple under this name. Two stars do the same for keyword arguments, collecting them into a dict whose keys are the argument names as strings. The names `args` and `kwargs` are pure convention; `def totals(*values, **labels)` uses exactly the same machinery. What matters is that `*values` is always a tuple (possibly empty) and `**labels` is always a dict (possibly empty), so the body never has to check whether extras were passed.
Position inside the parameter list is not cosmetic. `*args` swallows all remaining positional arguments, so any parameter written after it is unreachable positionally and can only be supplied by keyword. `**kwargs` must come last, because it is the catch-all for names, and Python needs every declared name resolved before deciding what is left over. That gives one legal ordering: ordinary parameters, `*args`, keyword-only parameters, `**kwargs`.
The star has a mirror meaning at the call site: there it spreads a collection out into separate arguments instead of collecting them. `point(*[1, 2, 3])` passes three positional arguments, and `point(**{"x": 1})` passes `x=1`. This symmetry is why `wrapper(*args, **kwargs)` followed by `func(*args, **kwargs)` forwards a call perfectly without knowing anything about `func`'s signature. The cost is that such a signature documents nothing, so use it for genuine pass-through code or truly variable arity, not as a way to avoid naming your parameters.
def tally(label, *scores, scale=1, **meta):
print("label:", label)
print("scores:", scores, type(scores).__name__)
print("meta:", meta, type(meta).__name__)
print("total:", sum(scores) * scale)
print("-")
tally("quiz", 3, 4, 5, scale=2, grader="ana", term="fall")
tally("solo", 10)In a def, * and ** collect the arguments no named parameter claimed into a tuple and a dict; in a call, they do the reverse and spread a sequence and a mapping back out.
Worked examples
Unpacking at the call site
The same star symbols spread a list or dict back into individual arguments.
def point(x, y, z):
return f"({x}, {y}, {z})"
coords = [1, 2, 3]
print(point(*coords))
opts = {"x": 7, "y": 8, "z": 9}
print(point(**opts))
print(point(*(4, 5), z=6))Example explained
Line 1`point` has three ordinary parameters and no *args at all, so unpacking is a caller-side feature and needs no cooperation from the function.
Line 2`*coords` sends 1, 2 and 3 as three positional arguments; if the list had two or four items it would be a TypeError.
Line 3`**opts` matches dict keys to parameter names, so key order in the dict is irrelevant.
Line 4`*(4, 5), z=6` mixes both styles: two positional values from the tuple, then z given by name.
Forwarding a call you know nothing about
A wrapper accepts any arguments and passes them through untouched.
def logged(func):
def wrapper(*args, **kwargs):
print(f"calling {func.__name__} with {args} {kwargs}")
result = func(*args, **kwargs)
print("->", result)
return result
return wrapper
logged
def area(width, height=2):
return width * height
area(5)
area(width=3, height=4)Example explained
Line 1`wrapper(*args, **kwargs)` accepts every possible call shape, so `logged` works on any function.
Line 2`func(*args, **kwargs)` re-spreads them, so `area` receives exactly what the caller wrote.
Line 3`area(5)` fills args with `(5,)` and leaves kwargs empty; `area(width=3, height=4)` does the opposite.
Line 4The split between args and kwargs is decided by how the call was written, not by area's own signature.
Anything after *args is keyword-only
A fourth positional value is silently absorbed instead of reaching the parameter that follows *extras.
def connect(host, *extras, port=5432):
return f"host={host} extras={extras} port={port}"
print(connect("db", "retry", "verbose", port=6000))
print(connect("db", 1, 2, 5432))Example explained
Line 1In the first call `port=6000` is given by name, the only way to reach it once `*extras` exists.
Line 2In the second call 5432 is just one more positional argument, so it joins `extras`.
Line 3`port` therefore keeps its default of 5432 and no error is raised, which makes this bug quiet and easy to miss.
Line 4Had `port` no default, the same call would raise TypeError about a missing keyword-only argument.
Important notes
`args` and `kwargs` are only naming conventions; the stars are the syntax, so `def render(*rows, **styles)` is the identical feature with clearer names.
`args` is a tuple and therefore immutable, so build a list if you need to change it; `kwargs` is a brand-new dict on every call, so mutating it never affects the caller's dictionary.
Common mistakes
Forwarding with `helper(args, kwargs)` instead of `helper(*args, **kwargs)`: the callee gets a tuple and a dict as two positional values, producing a TypeError or silently wrong data.
Assuming a parameter written after `*args` still accepts a positional value, as in `connect("db", 5432)` for `def connect(host, *extras, port=5432)`: the value lands in `extras`, `port` keeps its default, and nothing raises.
Calling `f(1, a=2)` for `def f(a, **kw)`: `**kw` cannot capture a name that a declared parameter owns, so Python raises TypeError about multiple values for argument 'a'.
Try it yourself
Change, predict, then run
Write `describe(name, *tags, sep=", ", **fields)` that prints the name, the tags joined by sep, and one "key=value" line per field. Call it once with literal arguments and once by unpacking a list of tags and a dict of fields.
Open the Python workspaceCheck your understanding
Given `def f(a, *rest, b=0): return a, rest, b`, what does `f(1, 2, 3)` return?
- (1, (2, 3), 0)
- (1, (2,), 3)
- (1, (2, 3), 3)
- TypeError: too many positional arguments
Show answer
`*rest` absorbs every positional argument after `a`, so both 2 and 3 go into the tuple and `b` keeps its default 0. `(1, (2,), 3)` assumes the last positional value can fall through to `b`, but a parameter declared after *args is keyword-only and can only be set as `b=3`.