Topic 5.2
Positional, Keyword and Default Arguments
In one line
Python can match call arguments to parameters by position, by name (keyword arguments), or fill in missing ones from defaults. Defaults are evaluated once when the function is defined, which makes a mutable default such as [] a classic trap.
Think of it like this
Ordering a pizza on a form. You can fill the boxes in order ("large, thin crust, extra cheese"), or label each answer ("size: large, extra cheese: yes") and skip the ones you don't care about, because the form has sensible defaults ("medium, regular crust"). Python lets you call functions in both styles.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Positional argument
- An argument matched to a parameter by its position in the call.
- Keyword argument
- An argument written
name=value, matched to the parameter with that name. - Default value
- A value written in the
defline (cups=1) that is used when the caller leaves the argument out. - Mutable
- An object that can be changed in place, such as a list or dict.
- Sentinel
- A special value used to mean "nothing was passed".
Noneis the usual one. - Signature
- The function name plus its parameter list, for example
make_tea(cups=1, sugar=0, milk=False). - SyntaxError
- Python refuses to run the file because the code breaks a grammar rule.
Step by step
01Positional: order decides
With only positional arguments, the first argument goes to the first parameter. Swap the arguments and you swap the meaning, with no error if both are numbers. This is why a call with many positional values is hard to read.
def power(base, exponent):
return base ** exponent
print(power(2, 5)) # 2 to the power 5 = 32
print(power(5, 2)) # 5 to the power 2 = 2502Keyword: names decide
Write name=value and the order stops mattering. The call tells the reader what each value means. You can mix styles, as long as all positional arguments come first.
def power(base, exponent):
return base ** exponent
print(power(exponent=5, base=2)) # 32, order is free
print(power(2, exponent=5)) # positional first, then keyword03Defaults: optional parameters
Give a parameter a default and the caller may leave it out. Put all required parameters first. Callers can then ask only for what they want to change.
def make_tea(cups=1, sugar=0, milk=False):
return f"{cups} cup(s), {sugar} sugar, milk={milk}"
print(make_tea()) # all defaults
print(make_tea(sugar=2)) # change only sugar
print(make_tea(4, milk=True)) # one positional, one keyword04When the default is created
Python evaluates the default expression once, when it executes the def line. The resulting object is stored on the function (you can see it in function.__defaults__). Every call that omits the argument receives that same stored object.
For an immutable default (0, False, "hello", None) that is harmless. For a list or dict it is dangerous, because append changes the one shared object.
05The trap in action
The second call below was meant to start with an empty basket. It didn't, because basket=[] was created only once. The stored default shows the pile-up directly.
def add_item_bad(item, basket=[]):
basket.append(item)
return basket
print(add_item_bad("apple")) # ['apple']
print(add_item_bad("pear")) # ['apple', 'pear'] <- surprise06The None fix
Use None as the default and build a new list inside the body each time. The check runs on every call, so each call gets its own list. If the caller passes their own list, it is used (and changed) as they expect.
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("apple")) # ['apple']
print(add_item("pear")) # ['pear']07Rules Python enforces
Defaults must trail required parameters. Positional arguments must precede keyword arguments in a call. A parameter cannot receive two values. A keyword that matches no parameter is a TypeError (unless the function accepts **kwargs, Topic 5.3). Each rule is checked before the body runs, so the error points at the call.
Try it yourself
- 1
Reorder keywords
In the first example, call
make_tea(milk=True, sugar=3, cups=2)and predict the output. Then change the order of the three keywords. Does the output change? - 2
Watch the trap grow
In the second example, call
add_item_bad("mango")a third time and printadd_item_bad.__defaults__again. The default list keeps growing, because it is the same object every time. - 3
Pass your own list
Make
my_basket = ["tea"]and calladd_item("milk", my_basket). Printmy_basket. The list you passed was changed, because the function received the same object, not a copy (Topic 4.9).
Code & diagrams
Every call below is valid. Work out which values go to which parameter before you read the output.
Expected output
1 cup(s), 0 sugar, milk=False
2 cup(s), 0 sugar, milk=False
2 cup(s), 1 sugar, milk=False
1 cup(s), 2 sugar, milk=False
3 cup(s), 0 sugar, milk=True
4 cup(s), 0 sugar, milk=TrueThe first function shares one list between calls. The second starts fresh each time. Look at the stored defaults at the end of each section.
Expected output
['apple']
['apple', 'pear']
stored default: (['apple', 'pear'],)
['apple']
['pear']
stored default: (None,)Real code builds requests with sensible defaults. The first function shares one headers dict between calls and leaks. The second uses None, copies what the caller gave it, and leaves the caller's dict alone.
Expected output
{'X-Call-0': '/users'}
{'X-Call-0': '/users', 'X-Call-1': '/orders'}
{'method': 'GET', 'path': '/users', 'headers': {'Accept': 'application/json'}, 'timeout': 10}
{'method': 'POST', 'path': '/orders', 'headers': {'Accept': 'application/json'}, 'timeout': 30}
{'Authorization': 'Bearer token', 'Accept': 'application/json'}
caller's dict unchanged: {'Authorization': 'Bearer token'}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A positional argument after a keyword argument
Write a keyword argument first and a plain value after it.
def make_tea(cups=1, sugar=0, milk=False):
return f"{cups} cup(s), {sugar} sugar, milk={milk}"
print(make_tea(sugar=1, 2))Myth vs fact
Myth
Default values are created again on every call.
Fact
They are created once, when def runs, and stored on the function. That is why a mutable default is shared between calls.
Myth
Keyword arguments are slower or less professional.
Fact
The cost is negligible, and keyword calls are the normal style in libraries and SDKs because they are self-explanatory and survive new options.
Myth
basket=[] is fine as long as you don't pass a list.
Fact
The shared default list is changed by every call that doesn't pass one, which is exactly the case where the bug shows up.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The stored defaults live in
func.__defaults__(a tuple) andfunc.__kwdefaults__(a dict, for keyword-only parameters, Topic 5.4). Some programmers use a mutable default on purpose as a cheap cache, but a closure (Topic 5.7) or a class (Phase 8) says it more clearly. - ▸
Linters flag the trap automatically (Ruff rule B006 and similar), so a good project catches it in CI (the automatic checks that run on every code change). Also guard against the same mistake with
datetime.now()orrandom.random()as a default: they are evaluated once at definition time, so every call sees the same value. - ▸
Use
is None, notif not basket, in the None pattern.not basketis also true for an empty list the caller passed in on purpose, and you'd silently replace it with a new one. - ▸
When
Noneis a meaningful value for a parameter, use a module-level sentinel such as_MISSING = object()and testif value is _MISSING. - ▸
The same trap, and the same cure, exists in classes: a dataclass refuses
items: list = []with aValueErrorand asks forfield(default_factory=list)(Topic 8.8). Pydantic copies mutable defaults for you, which is one reason people trust it, but a plain function does not. - ▸
A call can legally put a starred argument after a keyword:
f(y=2, *args)works, because Python collects all positional values first. It is rarely wise to write it. The strict rule is that a bare positional value cannot follow a keyword (f(y=2, 1)), and that a*spread cannot follow a**spread (f(**d, *a)). - ▸
A default cannot be computed from the other arguments (
def f(a, b=len(a))fails when thedefruns, becauseadoes not exist yet, or worse, silently picks up an unrelated global nameda). UseNoneand compute it in the body.
Remember this
- 1
A positional argument is matched to a parameter by its place: the first argument goes to the first parameter, and so on. A keyword argument is written
name=valueand is matched by the parameter's name, so its place in the call does not matter. Keyword calls make code self-explanatory:make_tea(sugar=2)says what the 2 is,make_tea(1, 2)does not. - 2
A default value is written in the
defline ascups=1. If the caller leaves that argument out, Python uses the default. Parameters with defaults must come after the ones without (def f(a, b=2)is fine,def f(a=1, b)is aSyntaxError), because otherwise Python could not tell which values go where. - 3
In a call, positional arguments must come before keyword arguments.
f(1, y=2)is fine;f(y=2, 1)is aSyntaxError. You also cannot give the same parameter twice, once by position and once by name: Python raisesTypeError: ... got multiple values for argument. - 4
Default values are created once, when the
defline runs, not each time the function is called. For numbers, strings andNonethis makes no difference, because they cannot be changed in place. For a mutable default (one that can be changed in place) such as a list or dictionary, every call that uses the default shares the same object, so changes pile up between calls. This is the famous mutable default argument trap. - 5
The fix is a habit: use
Noneas the default and create the fresh list inside the body (if basket is None: basket = []).Noneis the standard "nothing was given" signal, and the check usesis None(Topic 1.4). WhenNoneitself would be a valid input, use a private sentinel object instead (Topic 5.10). - 6
Keyword arguments are how almost every modern library is called, including every AI SDK (a ready-made toolkit of code for using a service):
client.chat(model="...", temperature=0.2, max_tokens=100). Heretemperaturesets how random the answer is andmax_tokenslimits how long it is. Models, options and flags have names, so calls stay readable and you can add a new option without breaking old calls.
Explain it without notes
Explain the mutable default argument trap and how to avoid it.
Which orderings of positional and keyword arguments are allowed in a call, and why?
Why must parameters with defaults come after parameters without?
When would you prefer keyword arguments over positional ones in a call?
Practice
Write net_price(price, discount=0.0, tax=0.18) that returns price * (1 - discount) * (1 + tax) rounded to 2 places. Print net_price(1000), then the price with a 10% discount (name the argument), then with no tax, then net_price(tax=0.05, price=200) to show that keyword order is free.
Write add_tag(tag, tags=None) that appends tag to the list and returns it, using the None fix. Call it twice and show that the second result does not contain the first tag.
Write shout(text, times=1, mark="!") that returns the text in capitals followed by the mark repeated times times. Call it with only text, then with times=3.
Trade-offs
- ↔
Many defaults versus few parameters. Defaults make simple calls short but hide behaviour; a function with eight optional parameters is hard to remember. Group related options (Topic 5.10) or split the function.
- ↔
None sentinel versus real default.
Noneplus anifis safe for mutable values but adds two lines. For immutable defaults (numbers, strings, booleans) use the real value; it is clearer and shows inhelp().
Done when you can
Done when you can call one function using positional, keyword, and default arguments.
Done when you can explain when a default value is created.
Done when you can spot a mutable default argument and rewrite it with
None.Done when you can say which call orders are legal and recognise the matching
SyntaxError.Done when you can read
make_tea(cups=1, sugar=0, milk=False)and say which arguments are optional.