1.2.28 · D3Introduction to Programming (Python)

Worked examples — Default parameters, keyword arguments

2,426 words11 min readBack to topic

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
Figure — Default parameters, keyword arguments

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

  1. Python has two slots: width, then height. Why this step? Slots are filled left to right by position — this is the default matching rule everything else builds on.
  2. 4 lands in width, 3 lands in height.
  3. 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

  1. name is filled by position with "Asha". Why this step? It is the first (non-default) slot, so the first argument goes here.
  2. greeting was not supplied. Python looks at the definition and sees greeting="Hello". Why this step? A missing default parameter falls back to the value baked into the def line — that is the whole point of a default.
  3. Return "Hello" + ", " + "Asha" = "Hello, Asha".

Verify: the output string is exactly Hello, Asha. ✓


Example 3 — C3: override a default positionally

Steps

  1. 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.
  2. Second slot greeting"Namaste" (position 2). This overrides the default "Hello". Why this step? A supplied positional argument always wins over the fallback.
  3. 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

  1. base5 by position. Why this step? first required slot.
  2. exp was skipped. We cannot reach mod positionally without first passing something for exp — so we jump straight to mod by 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.
  3. exp therefore keeps its default 2; mod7.
  4. Compute r = 5 ** 2 = 25, then since mod is not None, return 25 % 7.
  5. , so 25 % 7 = 4.

Verify: , and because , remainder . ✓

Figure — Default parameters, keyword arguments

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.


Steps

  1. width2, height5 by position. Why this step? All positional arguments are resolved first, left to right.
  2. x is never named or positioned past height, so it keeps its default 0. Why this step? untouched default → fallback.
  3. y=1 by keyword overrides its default 0.
  4. Result tuple (2, 5, 0, 1).

Verify: (width, height, x, y) == (2, 5, 0, 1). ✓ Note 2, 5 came before the keyword y=1 — Rule 2 satisfied.


Example 6 — C6: the illegal orderings (see the real errors)

Steps

  1. (a) Python reads the def line 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 fills a or b. To kill the ambiguity, this is a SyntaxError: non-default argument follows default argument — caught before the function even exists.
  2. (b) In the call, once width=2 is seen, a following bare 5 has no slot Python can trust. Why this fails: positionals must all resolve first (Rule 2). → SyntaxError: positional argument follows keyword argument.
  3. (c) "Asha" (position 1) already filled name; then name="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 one TypeError (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

  1. show(0): value0. label not passed → it is None → the guard replaces it with "value". Why this step? We test with is None, which asks "was it left out?"not "is it falsy?". So 0 in value is a perfectly real value, kept.
  2. Result: "value=0".
  3. show("", label=""): label was explicitly passed as "". "" is None is False. Why this step? The caller deliberately chose an empty string; is None correctly leaves it alone (whereas a sloppy if not label would wrongly overwrite it).
  4. Result: "=" — i.e. "={value}" with both empty → the string "=".

Verify: show(0) == "value=0" and show("", label="") == "=". ✓ The lesson (see Mutable vs immutable objects for why identity is differs from equality ==): "missing" is a distinct state from "falsy"; only is None distinguishes them.


Example 8 — C8: the mutable-default trap, traced call by call

Steps

  1. When the def line 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.
  2. add(1): no bag passed → the shared stored list is used; append 1[1].
  3. add(2): same stored list (still [1]) → append 2[1, 2].
  4. 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 bag

Now add(1)[1], add(2)[2], add(3)[3].

Verify (fixed): the three outputs are [1], [2], [3], independent. ✓

Figure — Default parameters, keyword arguments

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

  1. base untouched → default "cheese". Why this step? the customer kept cheese; omit it, let the fallback fire.
  2. 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).
  3. toppings=["mushroom"] — passed a real list, so the is None guard leaves it. Why the guard at all? to avoid the C8 trap; every order gets its own topping list.
  4. 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

  1. When def cap runs, Python evaluates ceiling=LIMIT right then. At that instant LIMIT is 10, so the default is frozen as the value 10. Why this step? Defaults capture the value at definition time, not a live link to the variable name.
  2. cap(50) (i): ceiling defaults to 10min(50, 10) = 10.
  3. We reassign LIMIT = 3. But the default already stored 10 — it does not re-read LIMIT.
  4. cap(50) (ii): ceiling is still 10min(50, 10) = 10 again.

Verify: both calls return 10; changing LIMIT afterward 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