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:

  1. Required positional parameters
  2. Defaults
  3. *args for extra positional
  4. **kwargs for 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.

Toggle theme (T)