Parameters, Arguments, Defaults, and Keywords
Key Takeaways
- Parameters are the names listed in a function definition; arguments are the actual values supplied during a call.
- Positional arguments bind left to right; keyword arguments name a specific parameter regardless of order.
- A required positional parameter cannot be omitted, and parameters with defaults must follow all required parameters.
- Default expressions are evaluated once when the def statement runs, not on every call, which is the mutable-default trap.
- Missing, duplicate, unexpected, or mis-ordered arguments all raise TypeError before the body runs.
Parameters versus arguments
A parameter is a name written in the function header. An argument is a value the caller supplies. PCEP deliberately uses meaningless names like a, b, and c so you cannot guess from semantics; you must bind the call mechanically.
def power(base, exponent):
return base ** exponent
print(power(2, 3))
Here base and exponent are parameters; 2 and 3 are arguments. The call is positional, so base gets 2, exponent gets 3, and the result is 8.
Positional and keyword binding
Arguments pass by position (matched left to right) or by keyword (the caller names the target parameter). Keyword arguments can reorder freely.
def describe(name, score, passed=False):
return name + ':' + str(score) + ':' + str(passed)
print(describe('Mia', 82))
print(describe(score=91, name='Sol', passed=True))
Both calls are valid. The first uses the default passed=False; the second rewrites the order but the keyword names keep the binding unambiguous, producing Sol:91:True.
Which call patterns are legal
| Call pattern | Valid? | Reason |
|---|---|---|
f(1, 2) | Yes | Positional values bind left to right |
f(a=1, b=2) | Yes | Keywords name the parameters |
f(a=1, 2) | No | A positional argument may not follow a keyword argument |
f(1, a=2) | Usually no | If the first parameter is a, it receives two values |
f(extra=1) | No, unless extra is a parameter | Unexpected keyword argument |
Every one of these binding failures raises TypeError, and it raises before any line of the body executes. A PCEP item may ask whether the code runs at all, or which exception class best describes the failure.
The ordering rule is strict: positional arguments must precede keyword arguments in a call. Mixing them the other way is a SyntaxError (e.g. f(a=1, 2)), which differs from the runtime TypeError you get from a duplicate or unexpected keyword. Knowing whether a snippet fails at parse time or call time is a recurring distinction.
Default values and the required-before-default rule
A parameter with a default becomes optional for the caller. Crucially, every parameter with a default must come after all required parameters in the header, or you get a SyntaxError.
def total(price, tax=0.10, discount=0):
return price + price * tax - discount
print(total(100)) # uses both defaults -> 110.0
print(total(100, discount=5)) # default tax, override discount -> 105.0
Defaults do not make earlier required parameters optional. For def f(a, b=2), calling f() still raises TypeError because a is unfilled; f(3) works and fills b from its default.
The mutable-default trap. Default expressions are evaluated once, when the def runs, not on each call. A mutable default such as [] is shared across calls:
def collect(item, bag=[]):
bag.append(item)
return bag
print(collect(1)) # [1]
print(collect(2)) # [1, 2] <- same list reused
The safe entry-level habit is to default to immutable values (numbers, strings, booleans, or None) and build the list inside the body.
Matching crowded calls by hand
When a call mixes positional and keyword arguments, write a binding table.
def mix(a, b=10, c=20):
return a + b * c
print(mix(2, c=3))
| Parameter | Bound value | Source |
|---|---|---|
a | 2 | positional |
b | 10 | default |
c | 3 | keyword |
The expression becomes 2 + 10 * 3. Following operator precedence (multiplication first), that is 2 + 30 = 32.
TypeError patterns to memorize
Watch for these recurring failures:
- Too few required arguments (
power(2)for a two-parameter function). - Too many positional arguments.
- The same parameter given twice, once positionally and once by keyword.
- An unexpected keyword the function never declared.
- A positional argument placed after a keyword argument.
When a snippet fails before its body starts, argument binding is almost always the cause. Trace the call header before tracing the body, and decide whether the error is SyntaxError (mis-ordered/keyword syntax) or TypeError (binding mismatch).
Arbitrary arguments: *args and **kwargs
PCEP-30-02 expects you to recognize, not necessarily design with, the two collecting parameters. A parameter written *args gathers any extra positional arguments into a tuple; a parameter written **kwargs gathers any extra keyword arguments into a dictionary.
def tally(first, *args, **kwargs):
return first, args, kwargs
print(tally(1, 2, 3, mode='fast'))
The output is (1, (2, 3), {'mode': 'fast'}). first takes the single required positional 1; the leftover positionals 2 and 3 land in the tuple args; the keyword mode='fast' lands in the dict kwargs. Knowing the container types (tuple for *, dict for **) is the most testable fact here.
| Form | Collects | Resulting type |
|---|---|---|
*args | extra positional arguments | tuple |
**kwargs | extra keyword arguments | dict |
Order in the header is fixed: ordinary parameters, then *args, then keyword-only parameters, then **kwargs. An empty call such as tally(9) simply yields (9, (), {}): the tuple and dict are present but empty, never None. A common distractor claims args is a list; it is always a tuple. Another distractor claims missing extras raise an error; they do not, the containers are just empty.
Why this matters for tracing: when a header contains * or **, count the required parameters first, then sweep the remaining positionals into the tuple and the remaining keywords into the dict. Get the partition right and the output follows directly.
Which statement best describes the difference between parameters and arguments?
For def f(a, b=4, c=5): return a + b + c, what does f(1, c=2) return?
Why is def collect(item, bag=[]): bag.append(item); return bag risky across multiple calls?