*args and **kwargs in Python, demystified
In Python, *args collects extra positional arguments into a tuple and **kwargs collects keyword arguments into a dict. Order rules, unpacking and errors.
In a Python function definition, *args collects any extra positional arguments into a tuple, and **kwargs collects any extra keyword arguments into a dictionary:
def show(*args, **kwargs):
print(args)
print(kwargs)
show(1, 2, colour="red")
(1, 2)
{'colour': 'red'}
That is the whole mechanism. The names args and kwargs are only a convention; the asterisks do the work. *numbers and **options behave identically and often read better.
The same stars do the opposite job when you call a function: * spreads a list out into separate arguments, and ** spreads a dictionary out into keyword arguments. Most confusion about *args and **kwargs comes from mixing up those two directions, so this post keeps them apart. Every output below was measured in Pyodide running Python 3.13.2.
*args: any number of positional arguments
def total(*numbers):
return sum(numbers)
print(total(1, 2, 3)) # 6
print(total()) # 0
Inside the function, numbers is a tuple: (1, 2, 3) in the first call and an empty () in the second. Calling with no arguments is allowed and gives an empty tuple, so a *args function has to handle "nothing was passed" itself.
Because it is a tuple, you cannot add to it or change it in place:
def collect(*args):
args.append(4)
collect(1, 2)
# AttributeError: 'tuple' object has no attribute 'append'
Convert it with items = list(args) first if you need a list to modify.
Defaults and *args together
A parameter with a default that sits before *args is filled by position before *args collects anything:
def f(a, b=2, *args):
return a, b, args
print(f(1)) # (1, 2, ())
print(f(1, 3, 4)) # (1, 3, (4,))
In the second call the 3 went into b, overriding its default, and only the 4 reached args. If the default is meant to be changed only on purpose, move it after *args, which makes it keyword-only. The next section explains why.
Passing a list by mistake
The classic mistake is passing a list you already have:
nums = [1, 2, 3]
total(nums)
# TypeError: unsupported operand type(s) for +: 'int' and 'list'
The list arrived as a single argument, so numbers was ([1, 2, 3],), a tuple with one item that happens to be a list. sum started from 0 and tried to add the list to it. The error message talks about int and list, nowhere near the real cause.
The fix is a star at the call site, which spreads the list into separate arguments:
print(total(*nums)) # 6
**kwargs: any number of keyword arguments
def describe(**details):
for key, value in details.items():
print(f"{key}: {value}")
describe(name="Ada", born=1815)
name: Ada
born: 1815
details is an ordinary dictionary, so everything in looping through a dictionary applies. Keyword arguments keep the order they were passed in: a function returning list(kwargs) for a call with z=1, a=2, m=3 gave ['z', 'a', 'm'].
The order rules
When a function takes several kinds of parameter, they must appear in this order:
def f(a, b=2, *args, c, d=4, **kwargs):
...
Regular parameters first, then *args, then keyword-only parameters, then **kwargs last. Break the order and Python refuses to compile the function:
| You wrote | Python says |
|---|---|
def f(**kw, *a): |
SyntaxError: arguments cannot follow var-keyword argument |
def f(*a, *b): |
SyntaxError: * argument may appear only once |
f(a=1, 2) (a call) |
SyntaxError: positional argument follows keyword argument |
Parameters after *args are keyword-only
Anything after *args can only be passed by name, because *args has already swallowed every remaining positional argument:
def tag(name, *children, sep=" "):
return f"<{name}>" + sep.join(children)
print(tag("p", "a", "b", sep="-")) # <p>a-b
print(tag("p", "a", "b", "-")) # <p>a b -
The second call is the trap. The "-" meant for sep was collected into children like everything else, and nothing complained. If a parameter after *args matters, give it no default, so a caller who forgets the name gets an error instead of a silent wrong answer.
A bare * with no name makes the parameters after it keyword-only without collecting anything:
def connect_to(*, host):
return host
connect_to("db")
# TypeError: connect_to() takes 0 positional arguments but 1 was given
That is a good way to force readable calls for arguments that are easy to confuse. The mirror image is /, which makes the parameters before it positional-only. Passing them by name raises a TypeError saying the function "got some positional-only arguments passed as keyword arguments".
Unpacking at the call site: * and **
Stars in a call spread values out. ** turns a dictionary into keyword arguments:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
options = {"name": "Ada", "greeting": "Hi"}
print(greet(**options)) # Hi, Ada!
The dictionary's keys must match the parameter names. An extra key is an error, as is supplying the same argument twice:
greet(**{"name": "Ada", "colour": "red"})
# TypeError: greet() got an unexpected keyword argument 'colour'
greet("Ada", name="Grace")
# TypeError: greet() got multiple values for argument 'name'
You have been using this from the other side all along. print is defined with *args, which is why it accepts any number of values and why a list can be spread into it:
print(*["a", "b", "c"], sep="-") # a-b-c
The same stars work outside function calls, too:
merged = {**{"a": 1, "b": 2}, **{"b": 3}}
print(merged) # {'a': 1, 'b': 3}
print([*range(3), *"ab"]) # [0, 1, 2, 'a', 'b']
first, *rest = [1, 2, 3, 4]
print(first, rest) # 1 [2, 3, 4]
When two dictionaries share a key, the later one wins, which is why b ended up as 3. In first, *rest, the starred name collects whatever is left over into a list.
The real use: functions that pass arguments on
The main reason *args and **kwargs exist is so a function can accept arguments it does not need to understand and hand them to another function untouched. A decorator is the standard example:
def log_calls(fn):
def wrapper(*args, **kwargs):
print(f"calling {fn.__name__} with {args} {kwargs}")
return fn(*args, **kwargs)
return wrapper
@log_calls
def area(width, height=1):
return width * height
print(area(3, height=4))
calling area with (3,) {'height': 4}
12
wrapper has no idea what area expects. It collects whatever it was given with *args, **kwargs in its definition, then spreads it back out with *args, **kwargs in the call. The same pair of lines appears in subclasses that pass arguments up with super().__init__(*args, **kwargs).
Notice the stars appear twice with opposite meanings: collecting in the def line, spreading in the return line. Once you see that, most uses of them read clearly.
Keep the wrapped function's name with functools.wraps
A wrapper written like this replaces the original function's identity. Measured with the inspect module, the wrapped area reported its name as wrapper, its signature as (*args, **kwargs) and its docstring as None. Every tool that reads those, from help() to your editor to error logs, now describes the wrapper instead of the function.
functools.wraps copies them across:
import functools
def log_calls(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
With that one line, the same checks reported area, (width, height=1) and the original docstring. Use it in every decorator you write.
Type hints for *args and **kwargs
The annotation on a starred parameter describes each item, not the whole collection:
def total(*numbers: int) -> int:
return sum(numbers)
*numbers: int means "any number of arguments, each an int". Inside the function, numbers is a tuple of ints. In the same way, **options: str means every keyword argument's value is a string. Writing *numbers: tuple[int, ...] is a common mistake: it says each argument is itself a tuple. Type checkers such as mypy read these annotations; Python itself does not enforce them at runtime.
When not to use them
A **kwargs parameter hides the function's real signature. Editors cannot autocomplete the options, help() shows nothing useful, and worst of all, a typo goes unnoticed:
def connect(host, **options):
timeout = options.get("timeout", 10)
return f"{host} timeout={timeout}"
print(connect("db", timout=1)) # db timeout=10
timout was misspelled, so the function quietly used its default of 10 and the caller's setting was ignored. With an explicit timeout=10 parameter, the same typo raises TypeError: connect() got an unexpected keyword argument 'timout' and you find it immediately.
So: name your parameters when you know what they are. Reach for *args and **kwargs when you genuinely accept a variable number of things, like total above, or when you are forwarding arguments to code you do not control, like the decorator. Python's tutorial section on arbitrary argument lists covers the full rules.
Quick reference
def f(*args): ... # extra positional args -> tuple
def f(**kwargs): ... # extra keyword args -> dict
def f(a, *args, key, **kw): ... # the required order
def f(*, key): ... # key must be passed by name
def f(a, /, b): ... # a must be passed by position
f(*items) # spread a list into arguments
f(**options) # spread a dict into keyword arguments
{**a, **b} # merge dicts; b wins on clashes
first, *rest = items # collect the remainder
Want to practise writing functions in lessons where the Python runs in your browser? Start the Python track.
More from the blog
What if __name__ == '__main__' actually does in Python
In Python, if __name__ == '__main__' runs code only when you run the file directly, not when it is imported. What __name__ holds and when you need it.
Read moreHow to reverse a list in Python (and a string)
To reverse a list in Python, use reverse() to change it in place, [::-1] for a reversed copy, or reversed() to loop backwards. Which to use and why.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.