Sometimes you want a function that accepts any number of arguments. Python has two special parameter forms for this: *args and **kwargs.
*args — collect extra positional arguments
A parameter prefixed with * collects any remaining positional arguments into a tuple:
def total(*numbers: int) -> int:
result: int = 0
for n in numbers:
result += n
return result
print(total(1, 2, 3)) # 6
print(total(1, 2, 3, 4, 5)) # 15
print(total()) # 0
Inside the function, numbers is a tuple of whatever was passed in.
The name args is just convention — it could be *numbers, *items, anything. But *args is so common that everyone recognises it.
**kwargs — collect extra keyword arguments
A parameter prefixed with ** collects any remaining keyword arguments into a dictionary:
def make_user(**fields: str) -> dict[str, str]:
return fields
user = make_user(name="Manikandan", role="admin", city="Chennai")
print(user)
{'name': 'Manikandan', 'role': 'admin', 'city': 'Chennai'}
Same convention — **kwargs is just the typical name.
Combining all four parameter kinds
A function can have:
- Required positional parameters
- Defaults
*argsfor extra positional**kwargsfor extra keyword
In that order:
def example(
first: str,
second: str = "default",
*args: str,
**kwargs: str,
) -> None:
print(f"first = {first}")
print(f"second = {second}")
print(f"args = {args}")
print(f"kwargs = {kwargs}")
example("a", "b", "c", "d", x="1", y="2")
first = a
second = b
args = ('c', 'd')
kwargs = {'x': '1', 'y': '2'}
You won’t often need this many at once. But it shows the order.
Unpacking — the other side of the same coin
* and ** also work at the call site, to spread a collection into individual arguments:
def add(a: int, b: int, c: int) -> int:
return a + b + c
nums: list[int] = [1, 2, 3]
print(add(*nums)) # same as add(1, 2, 3)
mapping: dict[str, int] = {"a": 1, "b": 2, "c": 3}
print(add(**mapping)) # same as add(a=1, b=2, c=3)
This is incredibly useful when you have arguments already in a collection.
A practical example — forwarding arguments
A common pattern in libraries is a wrapper function that accepts anything and passes it on:
def log_and_call(func, *args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
return func(*args, **kwargs)
The wrapper doesn’t care what shape func takes — it just forwards every argument. You’ll see this pattern in decorators (an advanced topic) and in test helpers.
Type hints with *args and **kwargs
The type hint after *args describes each element, not the tuple:
def total(*numbers: int) -> int: # each argument is an int
...
def join(*parts: str) -> str: # each argument is a str
...
Same for **kwargs — the hint describes each value in the dict:
def make_user(**fields: str) -> dict[str, str]:
return fields
If your kwargs can have mixed value types, use object or a union:
def event(**fields: str | int | bool) -> None:
...
When to use *args and **kwargs
Use them when:
- The function genuinely accepts a variable number of inputs (
sum,print,max) - You’re writing a wrapper that forwards arguments without knowing them in advance
Don’t use them just because they look powerful. A function with named, explicit parameters is almost always easier to understand than one that accepts a mystery bag of kwargs.
What’s next
You can accept any number of arguments. Next, an idea that sits behind every function call — scope: where variables live and how long they last.