Worked examples — Default parameters, keyword arguments
The scenario matrix
Think of a function call as filling slots left to right, some slots pre-loaded with defaults. The things that can vary are: how you fill the slots, whether you skip a middle one, and what "empty" input means. That gives us these case classes:
| Cell | Case class | What makes it tricky | Worked in |
|---|---|---|---|
| C1 | All positional, no defaults touched | baseline — nothing special | Ex 1 |
| C2 | Omit a default (use factory value) | prove the fallback fires | Ex 2 |
| C3 | Override a default positionally | order matters | Ex 3 |
| C4 | Skip a middle default, set a later one by keyword | the reason keywords exist | Ex 4 |
| C5 | Mix positional then keyword (Rule 2 boundary) | legal vs TypeError |
Ex 5 |
| C6 | Illegal orderings (both rules broken) | see the actual errors | Ex 6 |
| C7 | Degenerate input: None vs empty [] vs 0/"" |
falsy is not the same as missing | Ex 7 |
| C8 | Mutable-default trap + fix (state leaking across calls) | the famous bug, traced call-by-call | Ex 8 |
| C9 | Real-world word problem (pizza order builder) | translate English → call | Ex 9 |
| C10 | Exam twist: default evaluated once, at def-time | a global read at definition | Ex 10 |

The picture above is our mental model for the whole page: a row of slots in the definition, some pre-loaded (teal) with defaults, and a call that routes each value into a slot either by position (orange arrow, left-to-right) or by name (plum arrow, jumping straight to the labelled slot). Keep it in mind — every example below is just a different routing.
Example 1 — C1: baseline, all positional
Steps
- Python has two slots:
width, thenheight. Why this step? Slots are filled left to right by position — this is the default matching rule everything else builds on. 4lands inwidth,3lands inheight.- Return
4 * 3 = 12.
Verify: area of a 4-by-3 rectangle is . Units: (length)×(length) = area. ✓
Example 2 — C2: omit a default, factory value fires
Steps
nameis filled by position with"Asha". Why this step? It is the first (non-default) slot, so the first argument goes here.greetingwas not supplied. Python looks at the definition and seesgreeting="Hello". Why this step? A missing default parameter falls back to the value baked into thedefline — that is the whole point of a default.- Return
"Hello" + ", " + "Asha"="Hello, Asha".
Verify: the output string is exactly
Hello, Asha. ✓
Example 3 — C3: override a default positionally
Steps
- First slot
name←"Asha"(position 1). Why this step? Left-to-right filling is unaffected by defaults; the default just says what to use if nothing arrives. - Second slot
greeting←"Namaste"(position 2). This overrides the default"Hello". Why this step? A supplied positional argument always wins over the fallback. - Return
"Namaste, Asha".
Verify: output is
Namaste, Asha— greeting was overridden, name untouched. ✓
Example 4 — C4: skip a middle default, set a later one by name
This is the cell that justifies keyword arguments existing at all (see DRY principle - Don't Repeat Yourself — we refuse to re-type values we don't care about).
Steps
base←5by position. Why this step? first required slot.expwas skipped. We cannot reachmodpositionally without first passing something forexp— so we jump straight tomodby name. Why this step? A keyword argument routes by label, so it hops over the untouched middle slot. Positionally this is impossible; that impossibility is exactly why keywords were invented.exptherefore keeps its default2;mod←7.- Compute
r = 5 ** 2 = 25, then sincemodis notNone, return25 % 7. - , so
25 % 7 = 4.
Verify: , and because , remainder . ✓

The figure shows the plum keyword arrow leaping over the teal exp slot (which quietly holds its default 2) to land on mod. That leap is the superpower.
Example 5 — C5: legal mix (positional then keyword)
Steps
width←2,height←5by position. Why this step? All positional arguments are resolved first, left to right.xis never named or positioned past height, so it keeps its default0. Why this step? untouched default → fallback.y=1by keyword overrides its default0.- Result tuple
(2, 5, 0, 1).
Verify:
(width, height, x, y) == (2, 5, 0, 1). ✓ Note2, 5came before the keywordy=1— Rule 2 satisfied.
Example 6 — C6: the illegal orderings (see the real errors)
Steps
- (a) Python reads the
defline and sees a non-default (b) after a default (a=1). Why this fails: if a later call passed a bare positional, Python couldn't tell whether it fillsaorb. To kill the ambiguity, this is aSyntaxError: non-default argument follows default argument— caught before the function even exists. - (b) In the call, once
width=2is seen, a following bare5has no slot Python can trust. Why this fails: positionals must all resolve first (Rule 2). →SyntaxError: positional argument follows keyword argument. - (c)
"Asha"(position 1) already filledname; thenname="Asha"tries to fill it again. Why this fails: one slot, two values. →TypeError: greet() got multiple values for argument 'name'.
Verify: three different errors — two
SyntaxErrors (caught at parse/def time) and oneTypeError(caught at call time). The distinction matters: syntax errors stop the file from even loading. ✓
Example 7 — C7: degenerate inputs (None vs 0 vs "" vs [])
Steps
show(0):value←0.labelnot passed → it isNone→ the guard replaces it with"value". Why this step? We test withis None, which asks "was it left out?" — not "is it falsy?". So0invalueis a perfectly real value, kept.- Result:
"value=0". show("", label=""):labelwas explicitly passed as""."" is NoneisFalse. Why this step? The caller deliberately chose an empty string;is Nonecorrectly leaves it alone (whereas a sloppyif not labelwould wrongly overwrite it).- Result:
"="— i.e."={value}"with both empty → the string"=".
Verify:
show(0) == "value=0"andshow("", label="") == "=". ✓ The lesson (see Mutable vs immutable objects for why identityisdiffers from equality==): "missing" is a distinct state from "falsy"; onlyis Nonedistinguishes them.
Example 8 — C8: the mutable-default trap, traced call by call
Steps
- When the
defline runs once, Python builds the default list[]at that moment and stores it inside the function object. Why this step? Defaults are evaluated at definition time, not per call — this single fact is the whole bug. add(1): nobagpassed → the shared stored list is used; append1→[1].add(2): same stored list (still[1]) → append2→[1, 2].add(3): same list again →[1, 2, 3].
Verify: outputs are
[1],[1, 2],[1, 2, 3]— not three independent lists. ✓ Surprise confirmed.
The fix (None sentinel):
def add(item, bag=None):
if bag is None:
bag = [] # fresh list built INSIDE, every call
bag.append(item)
return bagNow add(1) → [1], add(2) → [2], add(3) → [3].
Verify (fixed): the three outputs are
[1],[2],[3], independent. ✓

Left panel: one shared bag filling up over three calls (the bug). Right panel: three fresh bags, one per call (the fix). Same shape as the pizza-topping-bowl metaphor from the parent note. Related idea: Variable scope - local vs global — the shared list lives with the function object, outliving each call's local scope.
Example 9 — C9: real-world word problem (pizza order builder)
Steps
baseuntouched → default"cheese". Why this step? the customer kept cheese; omit it, let the fallback fire.size="large"overrides"medium"by keyword. Why by keyword? self-documenting — the reader instantly sees which knob changed (parent note's "80/20" idea: change only the unusual).toppings=["mushroom"]— passed a real list, so theis Noneguard leaves it. Why the guard at all? to avoid the C8 trap; every order gets its own topping list.- Result:
"large cheese pizza + ['mushroom']".
Verify: output equals
large cheese pizza + ['mushroom']. ✓ Two knobs changed by name, one default kept — exactly the convenient common-case-plus-override pattern.
Example 10 — C10: exam twist — default is evaluated ONCE, at def-time
Steps
- When
def capruns, Python evaluatesceiling=LIMITright then. At that instantLIMITis10, so the default is frozen as the value10. Why this step? Defaults capture the value at definition time, not a live link to the variable name. cap(50)(i):ceilingdefaults to10→min(50, 10) = 10.- We reassign
LIMIT = 3. But the default already stored10— it does not re-readLIMIT. cap(50)(ii):ceilingis still10→min(50, 10) = 10again.
Verify: both calls return
10; changingLIMITafterward has no effect on the frozen default. ✓ This is the same "evaluated once at def-time" law that causes the C8 mutable trap — one law, two consequences.
Recall One law explains two famous surprises
Default evaluated at def-time explains BOTH the mutable-list trap (Ex 8) AND the frozen-global twist (Ex 10). ::: Yes — the default expression runs exactly once when def executes, capturing that value forever.
Connections
- Parent: Hinglish version
- Functions - def and return
- Variable scope - local vs global
- *args and **kwargs (variadic functions)
- Mutable vs immutable objects
- DRY principle - Don't Repeat Yourself