Python Part I · Module 11 · Functions
*args and **kwargs
Every function so far has known how many arguments it takes. Some genuinely cannot: a function that submits however many tests you name, a wrapper that passes options through to something else, or print itself, which accepts one value or nine with no change in how you call it. Python has a notation for that, and it is worth learning for two separate reasons. You will occasionally write it. You will constantly read it — the moment you look up a standard-library function, its signature is likely to contain a star, and not being able to read that line means not being able to read the documentation.
Intermediate16 min readPythonPart IFundamentalsFunctionsargs
Part I · Module 11 · Chapter 11.6 · *args and **kwargs
1. A Function That Cannot Know How Many
Submitting three tests with a fixed signature means choosing a number in advance:
def submit(test_a, test_b, test_c):
return [test_a, test_b, test_c]
print(submit("smoke", "reset", "burst"))['smoke', 'reset', 'burst']Two tests, or four, and the call fails with Chapter 11.3's TypeError. Passing a list instead works and shifts the brackets onto the caller:
def submit(tests):
return list(tests)
print(submit(["smoke", "reset", "burst"]))['smoke', 'reset', 'burst']That is a perfectly good design and is often the right one. But print does not make you write print(["a", "b"]), and this is how it avoids it.
2. One Star Collects the Extras
def submit(*tests):
print(tests, type(tests))
return len(tests)
print(submit("smoke", "reset", "burst"))('smoke', 'reset', 'burst') <class 'tuple'>
3*tests means collect every remaining positional argument into a tuple called tests. The caller writes a comma-separated list; the function receives one tuple.
Two stars do the same for named arguments, collecting them into a dictionary:
def run(*tests, **options):
print("tests :", tests, type(tests))
print("options:", options, type(options))
run("smoke", "reset", seed=1234, waves=True)tests : ('smoke', 'reset') <class 'tuple'>
options: {'seed': 1234, 'waves': True} <class 'dict'>Inside the body, tests is an ordinary tuple and options is an ordinary dictionary. Everything from Modules 6 and 8 applies — index them, loop them, call len() on them, use .items().
3. The Star Is the Syntax; the Name Is Yours
A persistent misconception: args and kwargs are not keywords. They are conventional names, and nothing depends on them:
def run(*tests, **options):
return len(tests), len(options)
print(run("a", "b", seed=1))(2, 1)That function is named *tests and **options throughout this chapter and works identically. The * and ** are the syntax.
Use the convention *args / **kwargs when the arguments are genuinely anonymous pass-through values, and a meaningful name whenever you can think of one. def submit(*tests) tells a reader what those values are; def submit(*args) does not.
4. What a Pass Hands You
Three cases worth knowing exactly, because two of them catch people:
def f(*items):
return items
print(f())
print(f("a"))
print(f("a", "b"))()
('a',)
('a', 'b')No arguments gives an empty tuple, not None. So a *args function never needs a guard before looping — Chapter 10.1 established that an empty collection runs the body zero times.
One argument gives a one-element tuple, with the trailing comma Chapter 6.2 insisted on. f("a") does not hand the body the string "a"; it hands it ("a",). Forgetting that is the usual first bug:
def shout(*words):
return words.upper()
print(shout("smoke"))AttributeError: 'tuple' object has no attribute 'upper'The body must work on the tuple, not on the value the caller happened to pass once.
5. The Order of the Four Kinds
A signature may contain all four kinds of parameter, and they must appear in this order:
def submit(test, *seeds, sim="vcs", **extra):
return (test, seeds, sim, extra)
print(submit("smoke"))
print(submit("smoke", 1, 2, 3))
print(submit("smoke", 1, 2, sim="xrun", waves=True, timeout=60))('smoke', (), 'vcs', {})
('smoke', (1, 2, 3), 'vcs', {})
('smoke', (1, 2), 'xrun', {'waves': True, 'timeout': 60})| Position | Kind | Example | Receives |
|---|---|---|---|
| 1 | required positional | test | the first argument |
| 2 | *name | *seeds | every remaining positional, as a tuple |
| 3 | keyword-only | sim="vcs" | only if named |
| 4 | **name | **extra | every remaining named argument, as a dict |
Note the third row. Anything after a * is keyword-only — that is the same rule Chapter 11.5 Section 7 got from a bare *, and *seeds does the same job while also collecting. In the last call, sim="xrun" reached its parameter and waves and timeout fell through to extra.
6. The Same Stars Unpack at the Call Site
This is the half most people meet later than they should. In a call, * and ** do the reverse: they spread a collection out into separate arguments.
def run_test(name, seed, sim):
return name + " " + str(seed) + " " + sim
args = ["smoke", 4210, "vcs"]
print(run_test(*args))
opts = {"name": "reset", "seed": 99, "sim": "xrun"}
print(run_test(**opts))smoke 4210 vcs
reset 99 xrunrun_test(*args) means take the items of args and pass them as separate positional arguments — it is run_test("smoke", 4210, "vcs"). And run_test(**opts) passes each key-value pair as a named argument.
Note that run_test has an ordinary signature with no stars in it. Unpacking is a property of the call, not of the function.
A mismatch is an ordinary TypeError, because unpacking happens before the call is matched:
def run_test(name, seed, sim):
return name
args = ["smoke", 4210]
print(run_test(*args))TypeError: run_test() missing 1 required positional argument: 'sim'Two items unpacked into three parameters — the same message Chapter 11.3 produced from a hand-written call, because by the time Python matched arguments to parameters there was no difference.
7. Reading print's Signature
Here is the payoff for reading rather than writing. Python documents print as:
print(*objects, sep=' ', end='\n')Every piece of that is now readable:
*objects— any number of positional arguments, collected into a tuple. That is whyprint("a", "b", "c")works with no brackets.sep=' '— keyword-only, because it follows the*; default a single space.end='\n'— keyword-only; default a newline.
So the behaviour you have relied on since Chapter 1.8 follows from a signature you can now decode:
print("a", "b", "c")
print("a", "b", "c", sep="-")
print("a", end=" | ")
print("b")a b c
a-b-c
a | bsep is what puts the spaces between the three values, and end is what normally puts the newline on. Neither could have been supplied positionally, because both sit after the *.
8. When Not to Use It
A star in your own signature should be a deliberate choice, not a way to avoid deciding:
def f(*args)when the function needs exactly two things removes theTypeErrorthat would have told the caller they got it wrong. The error was the useful part.**kwargsas a dumping ground hides what a function accepts. A reader cannot tell fromdef run(**options)whethertimeoutis a real option or a typo that will be silently ignored.- A list parameter is often clearer.
def submit(tests)called assubmit(["a", "b"])says the same thing as*testsand keeps the signature honest about there being one input.
The strong cases are genuine variadics (print, max) and pass-through wrappers that must forward whatever they were given without inspecting it — which is exactly where you will meet the notation in other people's code.
9. Predict, Run, Observe
# A
def f(*items):
return len(items), items
print("A:", f(), f(1), f(1, 2))
# B
def g(**opts):
return sorted(opts)
print("B:", g(b=1, a=2))
# C
def h(first, *rest):
return first, rest
print("C:", h("a", "b", "c"))
# D
def add(a, b):
return a + b
pair = [3, 4]
print("D:", add(*pair))The verified output:
A: (0, ()) (1, (1,)) (2, (1, 2))
B: ['a', 'b']
C: ('a', ('b', 'c'))
D: 7A — empty tuple, one-element tuple with its comma, two-element tuple.
B — looping or sorting a dictionary gives its keys (Chapter 10.3), so sorted(opts) is the key names.
C — first takes one argument and *rest collects what is left. This is the common split-the-head shape.
D — unpacking at the call site into an ordinary two-parameter function.
10. Exercises
Exercise 1 — collect them. Write total(*numbers) that adds any quantity of numbers and returns the sum, using the accumulator pattern from Chapter 10.10. Verify with zero, one and four arguments.
Exercise 2 — the one-element trap. Write a *words function that tries to call .upper() on its parameter directly. Record the error, then fix it so it uppercases each word.
Exercise 3 — name them well. Rewrite each signature with a meaningful name in place of the convention: (a) def submit(*args) for test names (b) def configure(**kwargs) for tool options (c) def log(*args) for message parts.
Exercise 4 — the four kinds. Write one signature containing a required positional, a *-collector, a keyword-only option and a **-collector. Call it three ways and print what each parameter received.
Exercise 5 — unpack. Given job = {"name": "smoke", "seed": 4210, "sim": "vcs"} and a function run_test(name, seed, sim), call it without writing any of the three values out. Then do the same from a list.
Exercise 6 — collect or spread. For each, say which meaning the star has: (a) def f(*x): (b) f(*items) (c) def g(**o): (d) g(**config).
Exercise 7 — decode a signature. Explain each part of def join(sep, *parts, strip=False): in one sentence, then write a call that supplies three parts and sets strip.
Exercise 8 — argue against. For each, say whether *args is the right choice and why: (a) a function that compares exactly two files (b) a function that prints a banner from any number of words (c) a wrapper that forwards all its arguments to another function (d) a function that takes a test name and a seed.
11. Summary
*namein a definition collects every remaining positional argument into a tuple;**namecollects every remaining named argument into a dict.- The
*is the syntax;argsandkwargsare only conventional names. Use a meaningful name when you have one. - No arguments gives an empty tuple, not
None— so no guard is needed before looping. - One argument gives a one-element tuple, which is the usual first bug: the body must work on the tuple.
- The four parameter kinds have a fixed order: required positional,
*-collector, keyword-only,**-collector. - Anything after a
*is keyword-only, whether the star is bare or named. - At a call site the same stars do the reverse —
f(*items)spreads a list into separate arguments,f(**opts)spreads a dict into named ones. - Unpacking is a property of the call, not of the function being called.
print(*objects, sep=' ', end='\n')is now readable, and explains the spacing and the newline you have relied on since Module 1.- Prefer an explicit list parameter unless the function is a genuine variadic or a pass-through wrapper.
The next chapter looks at what actually arrives when the argument is a list.
Where this fits
Part of the Python curriculum.
