Skip to main content

*args, **kwargs & unpacking

Level 2: Level 2: Idiomsmedium11 minargskwargsunpackingfunctions

Write functions that accept any number of arguments, and unpack collections into calls.

Why flexible signatures matter

Some functions cannot know their arity ahead of time. A decorator has to wrap a function whose signature it has never seen, so it must forward whatever it is given. A logging helper should accept log("saved", user, count) with as many values as the caller has. print, max, and str.format all take a variable number of arguments for the same reason. Without *args and **kwargs you would hard-code a fixed parameter count and rewrite the function every time a caller needs one more slot. These two features let one function absorb any call shape, and the mirror-image * and ** at the call site let you feed a collection you already hold into any function.

*args: collect extra positionals into a tuple

A parameter written *name sweeps up every positional argument that did not match an earlier parameter and binds them as a tuple:

def total(*nums):
    return sum(nums)   # nums is a tuple, e.g. (1, 2, 3)

total(1, 2, 3)   # 6
total()          # 0  (nums is the empty tuple ())

The name args is only a convention. What matters is the leading *. Because nums is a real tuple, sum(nums) works, and total() gives sum(()) == 0.

**kwargs: collect extra keywords into a dict

A parameter written **name gathers keyword arguments that did not match a named parameter into a dict:

def tag(name, **attrs):
    return name, attrs

tag("a", href="/x", id=3)   # ("a", {"href": "/x", "id": 3})

* and ** at the call site: spread a collection into a call

The same symbols run in reverse when you call a function. *seq spreads an iterable into positional arguments, and **mapping spreads a dict into keyword arguments. This is how you fill a template from a dict you already have:

values = {"name": "Ada", "age": 30}
"{name} is {age}".format(**values)   # "Ada is 30"

format(**values) is exactly format(name="Ada", age=30). One pair of symbols, two mirror roles: * and ** collect inside a def, and spread inside a call.

Check yourself

The same two symbols do opposite jobs depending on where they appear. Sort each line by which job the star is doing.

def total(*nums):
total(*nums)
def tag(name, **attrs):
template.format(**values)

Pitfall: passing a container where you meant to unpack it

Check yourself
total is defined as def total(*nums): return sum(nums). You already hold nums = [1, 2, 3] and write total(nums). What happens?
Table
*nums collects loose positional arguments into a tuple. With nums = [1, 2, 3], passing the list itself binds it as ONE element, so sum() sees a list inside a tuple and raises TypeError; spreading with *nums restores the three separate arguments.
callnums binds tosum(nums)
total(1, 2, 3)(1, 2, 3)6
total()()0
total(nums)([1, 2, 3],)TypeError
total(*nums)(1, 2, 3)6
*nums collects loose positional arguments into a tuple. With nums = [1, 2, 3], passing the list itself binds it as ONE element, so sum() sees a list inside a tuple and raises TypeError; spreading with *nums restores the three separate arguments.

total(*nums) collects loose numbers, not a list. If you already hold a list, you must spread it, or it arrives as a single argument:

nums = [1, 2, 3]
total(nums)    # nums bound as ONE arg -> sum(([1, 2, 3],)) -> TypeError
total(*nums)   # 6  (spreads to total(1, 2, 3))

The formatting side has its own traps. A placeholder in the template with no matching key raises KeyError, and a literal brace in the text must be doubled as {{ so format does not read it as a slot.

Check yourself
Given def f(a, *args, b): return a, args, b, what does the call f(1, 2, 3) do?

Interview nuance: any parameter listed after *args becomes keyword-only. It can no longer be filled positionally, because *args has already claimed every remaining positional argument. So def f(a, *args, b) requires b to be passed by name, and f(1, 2, 3) raises TypeError: f() missing 1 required keyword-only argument: 'b'. Interviewers use this to check that you understand the argument-binding order (named parameters fill first, *args sweeps the rest, then keyword-only parameters and **kwargs), not just the syntax.

Worked example (Python)
def total(*nums):
    return sum(nums)

print(total(1, 2, 3))    # 6
print(total())           # 0
print("{name} is {age}".format(**{"name": "Ada", "age": 30}))  # Ada is 30

Apply

Your turn

The task this lesson builds to.

Implement total(*nums): accept any number of numbers and return their sum.

total(1, 2, 3) is 6; total() is 0. Use a *nums parameter.

3 hints and 4 automated checks are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Implement render(template, values): fill the {placeholder} slots in template using the values dict.

For template = "{name} is {age}" and values = {"name": "Ada", "age": 30}, return "Ada is 30". Unpack the dict with **.

2 hints and 3 automated checks are waiting in the workspace.