Module 8 · Python Under the Hood ⏱ 18 min

Unpacking with *args and **kwargs

By the end of this lesson you will be able to:
  • Define a function that accepts any number of positional or keyword arguments
  • Explain the fixed ordering rule for mixed parameters in a signature
  • Distinguish packing in a parameter list from unpacking at a call site

You wrote max_of_two(a, b) to pick the larger of two numbers. Then someone needed three, then four, then a list of unknown length — and suddenly a tidy little function cannot grow another arm every time the input gets longer. Forcing every caller to wrap a single value in a list is just as clumsy.

Python solves this with two special parameters. *args collects any number of positional arguments into one tuple, and **kwargs collects any number of keyword arguments into one dictionary. One signature can then absorb two values, twenty, or none at all, without you predicting the count in advance. The same trick works in reverse when a function forwards its arguments to another — it cannot know in advance how many it will receive.

flowchart LR
  C["call add(1, 2, 3, 4)"] --> S["*args packs them"]
  S --> T["args = (1, 2, 3, 4)"]
  style S fill:#3776ab,color:#fff
  style T fill:#1e293b,color:#fff
*args gathers every positional argument the caller passes into a single tuple inside the function.

*args collects positionals into a tuple

The star in front of a parameter name tells Python to gather every leftover positional argument into a tuple. Inside the body, that parameter behaves like any other tuple — you can loop over it, index it, slice it, or pass it to sum, exactly as you would on a tuple you built by hand.

def add(*nums):
    return sum(nums)

Call add(1, 2, 3) and nums is the tuple (1, 2, 3); call add() and it is the empty tuple (). The star does its work only in the signature. Once execution enters the body, nums is just a tuple — the star has done its job and steps aside.

Print the gathered tuple, then its sum. Note that args is always a tuple, never a list.
def add(*nums):
    print("nums is:", nums)
    return sum(nums)

print("total:", add(1, 2, 3))
print("empty call:", add())

**kwargs collects keywords into a dict

Two stars do the same job for keyword arguments — the ones written as name=value. Every such pair is gathered into a dictionary, with the keyword as the key and the supplied value as the value.

def show(**options):
    return options

Calling show(theme="dark", verbose=True) hands you {"theme": "dark", "verbose": True}. The names args and kwargs are only convention; Python cares about the stars, not the spelling, so *nums or **config work just as well. Pick names that describe your data.

flowchart LR
  P["f(1, 2, 3)"] -->|"*args"| T["tuple (1, 2, 3)"]
  K["f(a=1, b=2)"] -->|"**kwargs"| D["dict {a: 1, b: 2}"]
  style T fill:#3776ab,color:#fff
  style D fill:#b45309,color:#fff
*args packs positionals into a tuple; **kwargs packs keywords into a dict. Same idea, two star counts.
A profile builder that accepts any keyword details and merges them into one dict.
def build_profile(name, **details):
    profile = {"name": name}
    profile.update(details)
    return profile

print(build_profile("Ada", role="analyst", born=1815))

Combining them — and the order rule

A signature can mix an ordinary parameter, *args, and **kwargs in the same definition. The catch is that the order is fixed, and Python enforces it strictly: plain positional parameters come first, then *args, then keyword parameters with defaults, and finally **kwargs at the very end.

def log(message, *tags, level="info", **extra):
    ...

Here message is required and positional, tags collects any extra positionals, level is a keyword with a default, and extra absorbs any remaining keywords. This fixed ordering is how Python decides where each argument belongs — without it, a call mixing positionals and keywords could not be mapped unambiguously. Swap two groups around and you get a SyntaxError before a line of the body ever runs.

Unpacking at the call site: the star spreads one collection into many arguments.
def add(a, b, c):
    return a + b + c

parts = [1, 2, 3]
print(add(*parts))          # unpacks the list into a, b, c

config = {"a": 10, "b": 20, "c": 30}
print(add(**config))        # unpacks the dict into keyword args

When to reach for them

*args and **kwargs shine when the count genuinely varies: a logging helper that tags a message with any number of labels, a configuration function that forwards unknown options to another layer, or a wrapper that passes everything it receives straight through to a function it wraps. For a fixed, known set of options, explicit named parameters are clearer — they show up in autocomplete and in error messages, where **kwargs is silent. A decorator that wraps another function typically accepts *args and **kwargs, forwards them unchanged, and returns the result; that pattern is how a decorator can wrap a signature it was never told about. Use these tools to absorb genuine variety, not to avoid deciding what your interface should be.

flowchart LR
  A["positional params"] --> B["*args"]
  B --> C["keyword defaults"]
  C --> D["**kwargs last"]
  V["def f(*args, a)"] -.->|"out of order"| E["SyntaxError"]
  style A fill:#3776ab,color:#fff
  style E fill:#b45309,color:#fff
The fixed order a signature must follow: positional parameters, then *args, then keyword defaults, then **kwargs.
Exercise

Write multiply_all(*nums) that accepts any number of arguments and returns their product. With no arguments at all, return 1 (the identity for multiplication).

def multiply_all(*nums):
    # your code here
    pass
Exercise

Tuple, not list. Predict the single printed line. Notice how args is shown.

def show(*args):
    print(args)

show(1, 2, 3)
Exercise

add_three expects three separate arguments, but from_list hands it the whole list as one — which is two arguments short of three. Fix the call so the list is unpacked into three arguments.

def add_three(a, b, c):
    return a + b + c

def from_list(parts):
    return add_three(parts)
Exercise

Inside a function defined as def f(*args), what is the type of args?

Exercise

Write describe(name, *tags, **details) that returns a dictionary with three keys: "name" (the required first argument), "tags" (a list made from the extra positional arguments), and "details" (a dict of the keyword arguments). This combines all three parameter kinds in the correct order.

def describe(name, *tags, **details):
    # your code here
    pass

Recap

  • *args gathers leftover positional arguments into a tuple; **kwargs gathers leftover keyword arguments into a dict.
  • The names are convention — the stars do the work, so *nums or **config are equally valid.
  • In a signature, order is fixed: positional parameters, then *args, then keyword defaults, then **kwargs.
  • The star packs in a parameter list but unpacks at a call site (add(*parts) spreads a list into separate arguments).

Next you will handle dates and times with the datetime module, where careful arithmetic stops you from reinventing the calendar by hand.

Checkpoint quiz

What does **kwargs collect inside a function?

Which signature is valid Python?

Go deeper — technical resources