Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner ~24 min· topic 3 of 10

Topic 5.3

*args, **kwargs and Unpacking in Calls

In one line

*args lets a function accept any number of positional arguments (collected into a tuple), and **kwargs any number of keyword arguments (collected into a dict). The same stars in a call do the opposite: they spread a list or dict into separate arguments, which is how wrappers forward arguments unchanged.

Think of it like this

A shopping bag at a market. The shopkeeper does not ask how many things you will buy before handing you a bag: whatever you put in goes into the same bag. *args is that bag for extra positional arguments, and **kwargs is a second bag for extra labelled ones ("brand=Amul", "size=large").

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Variadic
Accepting a variable number of arguments.
*args
A parameter that collects extra positional arguments into a tuple.
**kwargs
A parameter that collects extra keyword arguments into a dict. The name stands for "keyword arguments".
Unpacking
Spreading a list, tuple or dict into separate values using * or **.
Forwarding
Passing your own *args and **kwargs on to another function.
Iterable
Anything you can loop over, such as a list, tuple, string or range.
Wrapper
A function that calls another function and adds something around the call.

Step by step

01Collecting extra positionals with *

*numbers in the def line means: put every extra positional argument into a tuple called numbers. With no extras the tuple is empty, so the function works with zero arguments too.

main.pywhole filepython
def total(*numbers):
    print("numbers is", numbers)
    return sum(numbers)


print(total())          # numbers is ()
print(total(1, 2, 3))   # numbers is (1, 2, 3)

02Collecting keyword arguments with **

**details gathers every keyword argument that doesn't match a named parameter into a dict. Iterate it with .items() (Topic 4.4) to see all the names and values.

main.pywhole filepython
def describe(**details):
    for key, value in details.items():
        print(key, "=", value)


describe(name="Asha", age=12)

03Spreading values in a call

Stars in a call spread values out. total(*scores) is the same as total(4, 5, 6). describe(**point) is the same as describe(x=1, y=2). The spread values can be mixed with ordinary ones.

The values are copied into the new arguments; the original list or dict is not changed by the call, though the function may mutate objects inside it.

main.pywhole filepython
scores = [4, 5, 6]
print(total(*scores))              # total(4, 5, 6)
print(total(*scores, 10, *[20, 30]))

point = {"x": 1, "y": 2}
describe(**point, z=3)             # describe(x=1, y=2, z=3)

04Forwarding to another function

A wrapper that accepts *args, **kwargs can call any function without knowing its parameters. The positional arguments come out as a tuple, the keyword ones as a dict, and the stars in the inner call spread them back. (func.__name__ is the function's name as text. Here func is a parameter that holds a function; Topic 5.6 explains why that is allowed.)

main.pywhole filepython
def log_call(func, *args, **kwargs):
    print("calling", func.__name__, "with", args, kwargs)
    return func(*args, **kwargs)


def power(base, exponent=2):
    return base ** exponent


print(log_call(power, 3))
print(log_call(power, 2, exponent=10))
Forwarding to another functiondiagram
Rendering diagram…

05Mixing the kinds of parameter

Order in the def: ordinary parameters, *args, keyword-only, **kwargs. Python fills named parameters first; the bags take only what is left over. A name that appears in the call but matches a named parameter never reaches **kwargs.

One more rule: a keyword argument cannot repeat. Spreading a dict that contains a name you also wrote by hand (f(**options, size=3) when options has size) is a TypeError for multiple values.

06When not to use the bags

*args and **kwargs hide a function's real parameters: a reader, help(), an editor and a type checker (Phase 10) can no longer see what is accepted. Use them when the function genuinely forwards or accepts anything. For a function with a known shape, name the parameters.

Try it yourself

  1. 1

    Predict the types

    In the first example, change describe(name="Asha", age=12) to describe(). What is details now? Run it and check.

  2. 2

    Spread a string

    Call total(*"123"). Python spreads the string into "1", "2", "3". What error does sum give, and why? (Topic 1.6 explains str + int.)

  3. 3

    Forward with your own function

    Call log_call(max, 3, 9, 4) and log_call(print, "a", "b", sep="-"). The wrapper doesn't know either function's parameters, and still works.

Code & diagrams

Collect and spread New tab

The stars in the def collect. The stars in the call spread. Notice the types printed for numbers and details.

Sign in to run this example in your browser.

Expected output

numbers is () of type tuple
0
numbers is (1, 2, 3) of type tuple
6
details is {'name': 'Asha', 'age': 12} of type dict
numbers is (4, 5, 6) of type tuple
15
numbers is (4, 5, 6, 10, 20, 30) of type tuple
75
details is {'x': 1, 'y': 2, 'z': 3} of type dict
Forwarding and the shape of an SDK call New tab

log_call works for any function. create imitates how an AI SDK takes a dict of options with **.

Sign in to run this example in your browser.

Expected output

calling power with (3,) {}
9
calling power with (2,) {'exponent': 10}
1024
125
{'model': 'small-model', 'messages': 1, 'options': {'temperature': 0.2, 'max_tokens': 100}}
Order of kinds in a signaturepython
def f(a, b, *args, key=None, **kwargs):
    ...
# a, b      ordinary parameters
# *args     extra positional arguments (tuple)
# key       keyword-only parameter (Topic 5.4)
# **kwargs  extra keyword arguments (dict)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Pass a keyword to a function that only has *args

Call total(numbers=[1, 2, 3]) when total is defined with *numbers.

main.pywhole filepython
def total(*numbers):
    return sum(numbers)


print(total(numbers=[1, 2, 3]))
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 5, in <module>
print(total(numbers=[1, 2, 3]))
~~~~~^^^^^^^^^^^^^^^^^^^
TypeError: total() got an unexpected keyword argument 'numbers'

Myth vs fact

Myth

args and kwargs are special names.

Fact

Only the stars are special. *items and **options work identically; args and kwargs are a convention readers recognise.

Myth

*args is a list.

Fact

It is a tuple, so you can read it and loop over it, but you cannot append to it.

Myth

** in a call changes the original dictionary.

Fact

It only reads the dict and spreads its items into arguments. The dict itself is not modified by the call.

Myth

Using **kwargs everywhere makes code flexible and good.

Fact

It also makes code unclear and hides mistakes: a misspelled keyword is silently swallowed. Use it only to forward or collect truly open-ended options.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    CPython builds the tuple and dict for *args/**kwargs on each call, so a hot function with variadic parameters is a little slower than one with fixed parameters. This only matters in tight loops.

  • ▸

    * also unpacks in other places: building lists and tuples ([*a, *b]), sets, and dictionaries ({**defaults, **overrides}, where later keys win). This is the standard way to merge settings.

  • ▸

    Wrapper functions that use *args, **kwargs lose the wrapped function's name, docstring and signature. Decorators fix that with functools.wraps (Topics 9.5 and 9.6).

  • ▸

    f(*iterable) consumes the iterable at once into a tuple, so spreading a generator uses it up, and spreading a huge sequence copies it. Inside the function, kwargs is always a new dict: f(**d) never lets the function change the caller's d (the values themselves are shared, so a mutable value is still the same object). A ** spread accepts any string key, even "a b", if the function has **kwargs; a non-string key is TypeError: keywords must be strings.

  • ▸

    Typed forwarding (keeping the inner function's exact parameter types visible to a type checker) uses ParamSpec, covered in Phase 10. Without it, *args: Any, **kwargs: Any means "anything is accepted".

Remember this

  1. 1

    Put * before one parameter name (by convention args) and Python gathers all the extra positional arguments into a tuple under that name. def total(*numbers) can be called as total(), total(1) or total(1, 2, 3); inside, numbers is (), (1,) or (1, 2, 3). The names args and kwargs are only conventions; the stars do the work.

  2. 2

    Put ** before one parameter name (by convention kwargs) and Python gathers all the extra keyword arguments into a dict: names become keys, values become values. def describe(**details) called as describe(name="Asha", age=12) receives {'name': 'Asha', 'age': 12}. The dict keeps the order of the call.

  3. 3

    In a def line the order is fixed: ordinary parameters, then *args, then keyword-only parameters (Topic 5.4), then **kwargs. Each of *args and **kwargs may appear at most once. Values that match a named parameter go there; only the leftovers land in the bags.

  4. 4

    In a call, the stars work in reverse. f(*items) takes any iterable (a list, tuple, string, range) and spreads it into separate positional arguments. f(**options) takes a dict and spreads it into keyword arguments, where each key must be a string that names a parameter. You may use several stars in one call: f(*a, 10, *b, **c).

  5. 5

    The most important use of both stars is forwarding: a wrapper function takes *args, **kwargs and passes them on untouched with func(*args, **kwargs). It works for any function, whatever its parameters. Logging wrappers, retry helpers and decorators (Topic 9.6) are built this way.

  6. 6

    In AI work you see ** everywhere: SDK calls take many named options, and code keeps them in a dict (params = {"temperature": 0.2, "max_tokens": 100}) and calls client.create(model=name, messages=history, **params). When an AI model asks your program to run a function (a tool call), the request arrives as a dict of arguments, and tool(**arguments) runs it (Topic 5.6 shows this).

Explain it without notes

01

What do *args and **kwargs collect, and what types are they inside the function?

02

Explain what f(*items, **options) does in a call.

03

Why do wrapper functions take *args, **kwargs?

04

What is the order of the parameter kinds in a def line?

Practice

01

Write average(*numbers) that returns the mean of any number of values, or 0 when none are given. Print average(2, 4, 9) and average().

02

Write make_url(host, **query) that returns https://host?key=value&key=value using all keyword arguments in order. Call make_url("example.com", page=2, sort="new").

03

Write call_twice(func, *args, **kwargs) that calls func two times with the same arguments and returns a list of both results. Test it with pow or max.

Trade-offs

  • ↔

    Flexibility versus clarity. A variadic function accepts anything but documents nothing. Named parameters are verbose but self-explaining and checked.

  • ↔

    **Forwarding with **kwargs versus explicit pass-through.** Forwarding saves code and survives new options, but typos pass silently. For a public API, list the parameters you support.

Done when you can

  • Done when you can write a function that takes any number of positional arguments with *args.

  • Done when you can say what type *args and **kwargs are inside the function.

  • Done when you can call a function with *list and **dict.

  • Done when you can write a wrapper that forwards *args, **kwargs.

  • Done when you can explain why a keyword argument fails against a function with only *args.