*args, **kwargs, and unpacking
The asterisk means something different in five slightly different places — collecting arguments, collecting keyword arguments, and spreading a collection back out — and mixing them up is where most of the confusion actually comes from.
4 min read
The problem: a function that needs to accept "however many"
def add(a, b):
return a + b
add(1, 2, 3) # TypeError: add() takes 2 positional arguments but 3 were givenA normal function signature declares a fixed number of parameters, and calling it with the wrong count is an error — which is exactly right most of the time, but sometimes a function genuinely needs to accept an unknown, variable number of arguments. print() is the obvious built-in example: print(1), print(1, 2), and print(1, 2, 3, 4, 5) are all valid, and Python's own function signature has to somehow allow for that.
*args — collecting extra positional arguments into a tuple
def total(*args):
return sum(args)
total(1, 2, 3) # 6 — args is (1, 2, 3)
total(1, 2, 3, 4, 5) # 15 — args is (1, 2, 3, 4, 5)The * before a parameter name tells Python: "gather every remaining positional argument into a tuple with this name." args is just a convention, not a keyword — *numbers works exactly the same way — but *args is what you'll see in almost every codebase, and deviating from it without a reason just makes code harder to skim. Inside the function, args behaves like any other tuple: iterate over it, index into it, pass it to sum().
**kwargs — collecting extra keyword arguments into a dict
def describe(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
describe(name="Ada", age=36, field="mathematics")name: Ada
age: 36
field: mathematics
** before a parameter name does the same thing for keyword arguments — anything passed as key=value that doesn't match a named parameter gets collected into a dict named kwargs (again, a strong convention, not a requirement). This is what lets a function accept an open-ended, named set of options without declaring every possible one up front.
Combining fixed parameters with both
def create_user(username, *args, **kwargs):
print(f"username: {username}")
print(f"extra positional: {args}")
print(f"extra keyword: {kwargs}")
create_user("ada", "admin", role="mathematician", active=True)username: ada
extra positional: ('admin',)
extra keyword: {'role': 'mathematician', 'active': True}
Python requires a fixed order: regular positional parameters first, then *args, then **kwargs — this isn't a style preference, it's the only order Python's parser accepts. username is matched first because it's declared explicitly; everything else positional falls into args, everything else keyword falls into kwargs.
The other direction: * and ** to unpack, not collect
The same two symbols do the opposite job at a call site instead of a function definition — spreading a collection back out into individual arguments, rather than gathering individual arguments into a collection:
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
add(*numbers) # same as add(1, 2, 3) — the list is unpacked into three positional args
options = {"a": 1, "b": 2, "c": 3}
add(**options) # same as add(a=1, b=2, c=3) — the dict is unpacked into keyword argsThis is genuinely the same underlying idea in reverse: *args in a definition packs loose arguments into a tuple; * at a call site unpacks a tuple (or list, or any iterable) into loose arguments. The same relationship holds for ** and dicts. Confusing "packing" and "unpacking" is the single most common source of */** mistakes — the direction depends entirely on whether the asterisk appears in a def or in a call.
Where this shows up constantly in real code
Decorators, covered in their own lesson, rely on exactly this: a wrapper function is written as def wrapper(*args, **kwargs) specifically because it has no idea what arguments the function it's wrapping will be called with — collecting everything generically and passing it straight through (func(*args, **kwargs)) is what lets one decorator work on any function, regardless of that function's own signature. This is also how Django's class-based views and many library APIs forward arguments through layers of wrapping without needing to know each layer's exact signature.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. In `def total(*args): return sum(args)`, what type is args inside the function?
2. Why does `def create_user(username, *args, **kwargs):` require exactly that parameter order?
3. Given `numbers = [1, 2, 3]`, what does `add(*numbers)` actually do at the call site?
4. Why do decorator wrapper functions almost always use `def wrapper(*args, **kwargs):`?