*args and **kwargs

Collecting however many arguments a caller passes, and unpacking a list or dict back into a call.

Overview

Collecting

def total(*args):
    return sum(args)

total(1, 2, 3) gives args = (1, 2, 3). total() gives args = (). The function accepts any number of positional arguments without knowing in advance how many, and inside it args is an ordinary tuple.

Two stars do the same for keyword arguments:

def describe(**kwargs):

describe(name="ana", score=91) gives kwargs = {"name": "ana", "score": 91} — an ordinary dict.

The names are pure convention. *items and **options work identically; the stars carry the meaning. Convention is strong enough here that using different names in a general-purpose helper will raise eyebrows, but in a specific function a descriptive name often reads better.

args_kwargs.py

args_kwargs.py Python 3
Output

                    

unpacking_calls.py

unpacking_calls.py Python 3
Output

                    

Worth knowing

*args collects extra positional arguments into a tuple; **kwargs collects extra keyword ones into a dict.
The names are convention, not syntax. The stars are the syntax.
Order in a definition: normal, then *args, then **kwargs.
At a call site the stars do the reverse - they spread a list or dict back into arguments.

*args and **kwargs: A Practical Guide

One star collects any number of positional arguments into a tuple; two stars collect keyword arguments into a dictionary. At a call site the same stars run in reverse, spreading a collection back out into arguments.

Order in a definition

def report(label, *values, **options):

Named parameters first, then *args, then **kwargs. Python needs the fixed ones before the variable ones so it knows what to bind where.

Spreading

The same star at a call site does the opposite job:

dims = [2, 3, 4]
volume(*dims)    # same as volume(2, 3, 4)

Without the star you pass one argument — the list itself — and get a TypeError about missing parameters. With it, the list is spread across the parameters in order.

** does the same for a dict, matching keys to parameter names:

volume(**{"length": 2, "width": 3, "height": 4})

The pass-through pattern

This is where the two halves meet, and it is by far the most common real use:

def logged(func, *args, **kwargs):
    print("calling", func.__name__)
    return func(*args, **kwargs)

logged accepts anything and forwards it untouched. It does not need to know the signature of what it is wrapping. Every decorator, every wrapper, every "do this then call that" helper is built on this shape.

Merging collections

The stars also work when building literals:

[*first, *second]    {defaults, overrides}

The dict form is a neat way to layer configuration: later keys win, so overrides beat defaults.

Where you will actually meet them

Almost every use of *args and **kwargs in real code falls into one of three shapes, and recognising them makes the feature far less abstract.

The first is a function that genuinely takes an unknown number of things: print, max, os.path.join. These are rare to write and common to use.

The second is a wrapper. A decorator, a retry helper, a logging shim - anything that stands in front of another function and passes the call along. Here the star forms are not a convenience, they are the only way to write the wrapper without hard-coding the signature of everything it might wrap.

The third is a subclass extending its parent. def __init__(self, *args, extra=None, **kwargs) accepts whatever the parent accepts, adds one option of its own, and forwards the rest with super().__init__(*args, **kwargs). This keeps working when the parent gains a parameter, which is the whole point.

The cost of being too flexible

A signature of (*args, **kwargs) accepts everything, and that is its problem as well as its purpose. Nobody can tell what the function wants by reading it. An editor cannot autocomplete the call. A typo in a keyword name no longer raises a clear TypeError about an unexpected argument - it lands silently in kwargs and surfaces later as a missing key, or worse, as a default quietly being used instead of the value you thought you passed.

So the rule is about position in the system rather than taste. At the surface, where people call your code directly, name the parameters. In the middle, where you are forwarding a call you did not construct, use the stars. The further a function is from a human caller, the more flexibility costs you nothing.

Reading someone else's signature

When you meet def f(a, b=1, *args, c, **kwargs), read it in the order Python binds it. a is required and positional. b is positional with a default. args collects any further positional arguments. c comes after *args, which makes it keyword-only and, because it has no default, required by name. kwargs collects the rest.

The one that surprises people is c. Anything after *args can only be passed by keyword, whether or not it has a default. That is the mechanism behind keyword-only parameters, and it is why library authors sometimes write a bare * in a signature: it turns everything after it into a name you have to say out loud at the call site.

Keyword-only and positional-only, and why they exist

The stars do a second job besides collecting: they mark boundaries in a signature. Two markers control how callers are allowed to pass arguments.

A bare * makes everything after it keyword-only:

def connect(host, *, timeout=30, retries=3):
    return host, timeout, retries


print(connect("db", timeout=5))
try:
    connect("db", 5)
except TypeError as e:
    print("TypeError:", e)
('db', 5, 3)
TypeError: connect() takes 1 positional argument but 2 were given

The reason to want this is readability at the call site. connect("db", 5, 2) tells a reader nothing about what 5 and 2 mean, and it silently changes meaning if the parameter order is ever edited. Forcing the names makes the call self-documenting and makes reordering the parameters a safe change.

A / does the opposite, marking everything before it positional-only:

def distance(x, y, /):
    return abs(x - y)

Now distance(3, 5) works and distance(x=3, y=5) does not. This is rarer, and the reason is the mirror image: it keeps the parameter *names* out of the API, so they can be renamed later without breaking callers. Most builtins are positional-only for exactly that reason, which is why len(obj=x) fails.

The rule of thumb: make a parameter keyword-only when its meaning is not obvious from the call site, which in practice means flags, options and anything numeric that is not the main subject.

What the stars cost you

*args, **kwargs accepts everything, and the price is paid by every reader and every tool afterwards.

The signature stops documenting anything. def process(*args, **kwargs) tells the next person nothing about what to pass, and the only way to find out is to read the body — and then the body of whatever it forwards to.

Editors and type checkers lose their grip. Autocompletion has nothing to suggest, and a type checker cannot verify a call it cannot see the shape of. For a codebase using type hints, a star signature is a hole in the coverage that propagates to every caller.

Errors move. A misspelled keyword argument would normally raise TypeError: unexpected keyword argument 'timeuot' at the call, naming the mistake. Absorbed into **kwargs it becomes a missing key later, or — worse — a default silently used instead of the value you passed, which produces wrong output and no error at all.

None of that is an argument against the feature; it is an argument about where to use it. Wrappers and subclass forwarding need it and pay none of the cost, because there is no meaningful signature to state. A function people call directly should name its parameters.

Unpacking in the other direction

The two stars appear in three places and mean the same thing in all of them, which is easier to hold on to than three separate rules.

In a definition they collect: def f(*args) gathers loose positional arguments into a tuple.

At a call site they spread: f(*items) hands each item over as a separate argument.

In a literal they merge: [*a, *b] builds one list from two, and {**a, **b} builds one dictionary from two, with later keys winning.

The last is worth dwelling on because it has quietly become the standard way to combine collections:

defaults = {"colour": "red", "size": 1}
overrides = {"size": 3}

print({**defaults, **overrides})
print([*"ab", *"cd"])
{'colour': 'red', 'size': 3}
['a', 'b', 'c', 'd']

Both build a new object and leave the originals alone, which is the difference from update and extend. And in assignment the star runs backwards again: first, *rest = items collects rather than spreads, because the star is on the receiving side.

A decorator, concretely

The pass-through pattern exists mainly so that decorators can be written, and seeing one whole makes the pieces click:

import functools

def announce(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("calling", func.__name__, "with", args, kwargs)
        result = func(*args, **kwargs)
        print("->", result)
        return result
    return wrapper


@announce
def add(a, b=0):
    return a + b


print(add(2, b=3))
print(add.__name__)
calling add with (2,) {'b': 3}
-> 5
5
add

Four things are happening. announce takes a function and returns a replacement. wrapper accepts anything with *args, **kwargs, so it can stand in front of any function at all. The forwarding call func(*args, **kwargs) spreads them back out, so add receives exactly what the caller wrote. And @announce above def add is shorthand for add = announce(add).

functools.wraps is the part people leave out and then miss. Without it, add.__name__ prints wrapper, the docstring is gone, and every traceback through the decorated function names the wrapper instead of the function. It copies the identifying attributes across, and it is one line.

Note how the arguments arrive: 2 in args and b=3 in kwargs, exactly as the caller passed them. The wrapper does not know or need to know that add has a parameter called b.

Asking a function what it accepts

When you do need to know the signature — validating a plugin, building a command-line interface, writing a framework — inspect answers it rather than parsing the source:

inspect.signature(func) returns an object listing the parameters, their defaults, their kinds (positional, keyword-only, *args, **kwargs) and any annotations. sig.bind(*args, **kwargs) matches a proposed call against it and raises the same TypeError the real call would, which is how you check arguments before doing expensive work.

This is also how a decorator can be smarter than pure forwarding. A caching decorator that wants a stable key needs to know that f(1) and f(a=1) are the same call; sig.bind plus apply_defaults normalises both to the same thing, which no amount of inspecting args and kwargs directly will do.

The star in assignment

The same star appears in assignment, and it is worth connecting to the rest rather than learning separately.

first, *rest = items binds the first item and collects everything else into a list. *most, last = items does the mirror image. a, *middle, b = items takes both ends and collects what is between them. In each case the starred name gets a list — always a list, even when the right-hand side was a tuple or a string.

This is the collecting sense of the star, the same as in a definition: the star marks the name that absorbs however many items are left over. The spreading sense is what you get on the other side of the equals sign, in a call or a literal.

Only one star is allowed per assignment, for the obvious reason that two would make the split ambiguous. And the non-starred names are still required: unpacking a, *rest = [] raises, because there is nothing for a, while *rest, = [] succeeds and gives an empty list.

Questions people ask

Do the names args and kwargs matter? No, the stars carry the meaning. The names are a strong convention in general-purpose wrappers and worth replacing with something descriptive elsewhere.

Can I have *args without **kwargs? Yes, and the reverse. They are independent.

What order do they go in? Named parameters, then *args, then keyword-only parameters, then **kwargs.

Is **kwargs ordered? Yes, it is a normal dictionary and keeps the order the caller used.

Can I pass a list where *args is expected? Only with a star: f(*items). Without it you pass the list as a single argument.

Why does f(**d) fail with "keywords must be strings"? Because a dictionary with non-string keys cannot be turned into keyword arguments.

How do I forward everything including the function's own new option? Take it as keyword-only, and forward *args, **kwargs unchanged: the new option is bound by name and never reaches the wrapped call.

Recap in one screen

  • One star collects positional arguments into a tuple; two collect keyword arguments into a dictionary.
  • The same stars at a call site spread a collection back into arguments, and in a literal they merge collections.
  • A bare * in a signature makes what follows keyword-only; a / makes what precedes it positional-only.
  • The pass-through f(*args, **kwargs) is what makes decorators and wrappers possible without knowing any signature.
  • A star signature costs documentation, tooling and clear errors — use it where there is no meaningful signature to state, not to avoid writing one.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Inside `def f(*args)`, what type is `args`?

  2. `volume(*[2, 3, 4])` is the same as what?

  3. Why does the wrapper pattern use both `*args` and `**kwargs`?

Cheat sheet

*args and **kwargs

One star collects any number of positional arguments into a tuple; two stars collect keyword arguments into a dictionary. At a call site the same stars run in reverse, spreading a collection back out into arguments.

PYTHON · vizlearn.in/python/args_and_kwargs.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.