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
*argsand**kwargson 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.
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.
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.
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.)
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))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
Predict the types
In the first example, change
describe(name="Asha", age=12)todescribe(). What isdetailsnow? Run it and check. - 2
Spread a string
Call
total(*"123"). Python spreads the string into"1","2","3". What error doessumgive, and why? (Topic 1.6 explainsstr + int.) - 3
Forward with your own function
Call
log_call(max, 3, 9, 4)andlog_call(print, "a", "b", sep="-"). The wrapper doesn't know either function's parameters, and still works.
Code & diagrams
The stars in the def collect. The stars in the call spread. Notice the types printed for numbers and details.
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 dictlog_call works for any function. create imitates how an AI SDK takes a dict of options with **.
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}}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.
def total(*numbers):
return sum(numbers)
print(total(numbers=[1, 2, 3]))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/**kwargson 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, **kwargslose the wrapped function's name, docstring and signature. Decorators fix that withfunctools.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,kwargsis always a new dict:f(**d)never lets the function change the caller'sd(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 isTypeError: 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: Anymeans "anything is accepted".
Remember this
- 1
Put
*before one parameter name (by conventionargs) and Python gathers all the extra positional arguments into a tuple under that name.def total(*numbers)can be called astotal(),total(1)ortotal(1, 2, 3); inside,numbersis(),(1,)or(1, 2, 3). The namesargsandkwargsare only conventions; the stars do the work. - 2
Put
**before one parameter name (by conventionkwargs) and Python gathers all the extra keyword arguments into a dict: names become keys, values become values.def describe(**details)called asdescribe(name="Asha", age=12)receives{'name': 'Asha', 'age': 12}. The dict keeps the order of the call. - 3
In a
defline the order is fixed: ordinary parameters, then*args, then keyword-only parameters (Topic 5.4), then**kwargs. Each of*argsand**kwargsmay appear at most once. Values that match a named parameter go there; only the leftovers land in the bags. - 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
The most important use of both stars is forwarding: a wrapper function takes
*args, **kwargsand passes them on untouched withfunc(*args, **kwargs). It works for any function, whatever its parameters. Logging wrappers, retry helpers and decorators (Topic 9.6) are built this way. - 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 callsclient.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, andtool(**arguments)runs it (Topic 5.6 shows this).
Explain it without notes
What do *args and **kwargs collect, and what types are they inside the function?
Explain what f(*items, **options) does in a call.
Why do wrapper functions take *args, **kwargs?
What is the order of the parameter kinds in a def line?
Practice
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().
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").
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
**kwargsversus 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
*argsand**kwargsare inside the function.Done when you can call a function with
*listand**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.