Python Part I · Module 11 · Functions
Defaults and Keyword Arguments
Chapter 11.4 said everything a function needs should arrive as an argument, and a function that genuinely needs six things will have six parameters. That is correct and it creates a new problem: most calls want the ordinary value for most of them, and a call that supplies all six in positional order is a row of bare values that nobody can read. Python solves both halves — parameters can carry a default so the caller may omit them, and arguments can be supplied by name so the call site says what each one means. One rule about when defaults are evaluated has a consequence famous enough that it has its own name, and this chapter arrives at it from the rule rather than presenting it as a curiosity.
Foundation16 min readPythonPart IFundamentalsFunctionsArguments
Part I · Module 11 · Chapter 11.5 · Defaults and Keyword Arguments
1. Most Calls Want the Ordinary Value
A function that submits a test genuinely needs four things: which test, which seed, which simulator, and whether to dump waves. With Chapter 11.4's rule, that is four parameters — and every call must supply all four:
def run_test(name, seed, sim, waves):
return name + " seed=" + str(seed) + " sim=" + sim + " waves=" + str(waves)
print(run_test("smoke", 1, "vcs", False))
print(run_test("reset", 1, "vcs", False))
print(run_test("burst", 99, "vcs", True))smoke seed=1 sim=vcs waves=False
reset seed=1 sim=vcs waves=False
burst seed=99 sim=vcs waves=TrueTwo of those three calls say 1, "vcs", False — the ordinary values — and the third differs in only two of the four. The repetition is noise, and worse, it means adding a fifth parameter later requires editing every existing call.
2. A Default Makes a Parameter Optional
def run_test(name, seed=1, sim="vcs", waves=False):
return name + " seed=" + str(seed) + " sim=" + sim + " waves=" + str(waves)
print(run_test("smoke"))
print(run_test("smoke", 99))
print(run_test("smoke", 99, "xrun"))smoke seed=1 sim=vcs waves=False
smoke seed=99 sim=vcs waves=False
smoke seed=99 sim=xrun waves=Falsename has no default, so it is still required. The other three may be omitted, and the value in the header is used.
Parameters with defaults must come after those without. Python needs to know which positional argument fills which parameter, and that is impossible if a required parameter sits behind an optional one:
def run_test(seed=1, name):
return name
print(run_test("smoke"))SyntaxError: parameter without a default follows parameter with a defaultCaught before anything runs, like every SyntaxError.
3. Positional Calls Stop Being Readable
Defaults fix the repetition. They do not fix this:
def run_test(name, seed=1, sim="vcs", waves=False):
return name + " seed=" + str(seed) + " sim=" + sim + " waves=" + str(waves)
print(run_test("burst", 99, "xrun", True))burst seed=99 sim=xrun waves=TrueRead run_test("burst", 99, "xrun", True) as a reviewer would, without the definition in front of you. 99 is a number — a seed, a timeout, a repeat count? And True is the worst of them: a bare boolean at a call site carries no information at all about which option it is switching on.
4. Name the Argument
Any argument may be supplied by name:
def run_test(name, seed=1, sim="vcs", waves=False):
return name + " seed=" + str(seed) + " sim=" + sim + " waves=" + str(waves)
print(run_test("smoke", seed=99, waves=True))
print(run_test("smoke", sim="xrun"))
print(run_test(seed=7, name="reset"))smoke seed=99 sim=vcs waves=True
smoke seed=1 sim=xrun waves=False
reset seed=7 sim=vcs waves=FalseThree things to notice.
The first call sets seed and waves and skips sim entirely — naming arguments lets you fill any subset, in any combination, without supplying the ones in between.
The second call changes only the simulator, and reads as a sentence.
The third supplies both arguments by name and in the wrong order, which is fine: once an argument is named, position stops mattering.
5. Positional Arguments Come First
Once you start naming, everything after must also be named:
def run_test(name, seed=1):
return name
print(run_test(name="smoke", 99))SyntaxError: positional argument follows keyword argumentPython matches positional arguments by counting, and a named argument breaks the count. Again a SyntaxError — this is a rule about how calls may be written, so it is caught at parse time rather than when the call runs.
6. A Default Is Evaluated Once, at Definition
Here is the rule that surprises people, and it follows directly from Chapter 11.2 Section 4: a def statement runs when execution reaches it. The default expressions in the header are part of that statement, so they are evaluated then — once — and the resulting value is kept and reused for every later call.
start = 10
def show(value=start):
return value
print(show())
start = 99
print(show())
print(show(start))10
10
99The second call still returns 10. The default was fixed at 10 when the def ran; reassigning start afterwards changes nothing, because the header is not re-read.
With an immutable default like 10 that is merely surprising. With a mutable default it is a real bug:
def add(item, bucket=[]):
bucket.append(item)
return bucket
print(add("a"))
print(add("b"))
print(add("c"))['a']
['a', 'b']
['a', 'b', 'c']Three independent calls, and the list keeps growing. There is only one list — created once, when the def ran — and every call that omits bucket appends to that same one. A function intended to start fresh each time accumulates across the entire life of the program.
The fix is a None sentinel, using Chapter 9.3's is None:
def add(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(add("a"))
print(add("b"))
print(add("c", ["existing"]))['a']
['b']
['existing', 'c']None is immutable, so sharing it is harmless, and the if creates a genuinely new list on each call that needs one. The third call shows the parameter still works normally when supplied.
Safe defaults are immutable values: numbers, strings, booleans, None, and tuples.
7. Making a Parameter Keyword-Only
Section 4 recommends naming booleans; a bare * in the header requires it:
def run(name, *, waves=False, seed=1):
return name + " waves=" + str(waves) + " seed=" + str(seed)
print(run("smoke", waves=True))
print(run("smoke", seed=42, waves=True))smoke waves=True seed=1
smoke waves=True seed=42Note the second line: the call wrote seed=42 first and waves=True second, and the output still reads waves=... seed=..., because that order is decided by the function body, not by the call. Naming arguments frees the call site from positional order; it changes nothing inside.
Everything after the * can only be supplied by name. Try it positionally:
def run(name, *, waves=False, seed=1):
return name
print(run("smoke", True))TypeError: run() takes 1 positional argument but 2 were givenThe message counts only the parameters before the * as positional, which is exactly what the * declared.
This is worth using for options — anything the caller is choosing rather than supplying as data. It converts a convention a reviewer has to enforce into a rule the language enforces.
8. One Signature, Four Call Forms
For def run_test(name, seed=1, sim="vcs", waves=False):
| Call | name | seed | sim | waves | Readable? |
|---|---|---|---|---|---|
run_test("smoke") | smoke | 1 | vcs | False | yes |
run_test("smoke", 99) | smoke | 99 | vcs | False | borderline |
run_test("smoke", 99, "xrun", True) | smoke | 99 | xrun | True | no |
run_test("smoke", seed=99, waves=True) | smoke | 99 | vcs | True | yes |
The last row is the one to write. It supplies exactly what differs from the ordinary case, names each one, and skips sim without a placeholder.
9. Predict, Run, Observe
# A
def greet(name, greeting="hello"):
return greeting + ", " + name
print("A:", greet("axi"), "|", greet("axi", "hi"), "|", greet(greeting="hey", name="axi"))
# B
LIMIT = 5
def cap(n, top=LIMIT):
return top if n > top else n
LIMIT = 100
print("B:", cap(50))
# C
def collect(x, into=None):
if into is None:
into = []
into.append(x)
return into
print("C:", collect(1), collect(2))
# D
def tag(name, *, strict=False):
return name + ("!" if strict else "")
print("D:", tag("a"), tag("a", strict=True))The verified output:
A: hello, axi | hi, axi | hey, axi
B: 5
C: [1] [2]
D: a a!A — default used, default overridden positionally, both supplied by name and out of order.
B — 5, not 50 and not 100. The default was fixed at 5 when the def ran, so reassigning LIMIT to 100 afterwards had no effect, and the conditional expression caps 50 at 5.
C — two separate lists, because the sentinel creates a new one per call. Replace None with [] and these become [1] and [1, 2].
D — strict is keyword-only, and a conditional expression from Chapter 9.6 chooses the suffix.
10. Exercises
Exercise 1 — add defaults. Take def connect(host, port, timeout, retries): and give the last three sensible defaults. Then write the shortest call that changes only the number of retries.
Exercise 2 — order the parameters. Write a header with a required parameter after an optional one and record the exact error. Explain in one sentence why Python cannot allow it.
Exercise 3 — name the booleans. Rewrite each call so a reviewer can read it: (a) run("smoke", True, False) (b) report(results, True) (c) parse(text, False, True, 3).
Exercise 4 — the frozen default. Write a function whose default comes from a module-level name, call it, reassign that name, and call it again. Record both results and explain them in two sentences using the words "when the def ran".
Exercise 5 — the growing list. Run Section 6's add three times. Predict the fourth call's output before running it. Then apply the sentinel fix and predict again.
Exercise 6 — safe or not. For each default, say whether it is safe and why: (a) count=0 (b) names=[] (c) sep=", " (d) opts={} (e) tags=() (f) found=None.
Exercise 7 — keyword-only. Write submit(test, *, seed=1, waves=False) and show both a working call and the positional call that fails. Record the error and explain what the * declared.
Exercise 8 — read the call. Given def sweep(test, start=0, count=10, step=1, sim="vcs"):, say what each of these does, and rewrite any that are unreadable: (a) sweep("smoke") (b) sweep("smoke", 4210, 3) (c) sweep("smoke", 4210, 3, 2, "xrun") (d) sweep("smoke", count=3, sim="xrun").
11. Summary
- A default makes a parameter optional — the caller may omit it and the header's value is used.
- Parameters with defaults must follow those without, or it is a
SyntaxError. - Arguments may be supplied by name, which lets you fill any subset in any order and skip the ones in between.
- Name every boolean at the call site. A bare
Trueis a fact with its label removed. - Positional arguments must come before named ones, also a
SyntaxError. - A default is evaluated once, when the
defruns — not on each call. Reassigning what it came from afterwards changes nothing. - A mutable default is shared by every call that omits it.
bucket=[]grows across calls, and the first call looks perfect. - Use
Noneas the sentinel and create the real value inside the body withif x is None:. - Safe defaults are immutable: numbers, strings, booleans,
None, tuples. - A bare
*makes everything after it keyword-only, turning a review convention into a language rule.
The next chapter removes the requirement to know how many arguments there will be.
Where this fits
Part of the Python curriculum.
