Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner ~26 min· topic 2 of 10

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 def line (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". None is 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.

main.pywhole filepython
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 = 25

02Keyword: 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.

main.pywhole filepython
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 keyword

03Defaults: 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.

main.pywhole filepython
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 keyword

04When 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.

When the default is createddiagram
Rendering diagram…

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.

main.pywhole filepython
def add_item_bad(item, basket=[]):
    basket.append(item)
    return basket


print(add_item_bad("apple"))    # ['apple']
print(add_item_bad("pear"))     # ['apple', 'pear']  <- surprise

06The 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.

main.pywhole filepython
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. 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. 2

    Watch the trap grow

    In the second example, call add_item_bad("mango") a third time and print add_item_bad.__defaults__ again. The default list keeps growing, because it is the same object every time.

  3. 3

    Pass your own list

    Make my_basket = ["tea"] and call add_item("milk", my_basket). Print my_basket. The list you passed was changed, because the function received the same object, not a copy (Topic 4.9).

Code & diagrams

Positional, keyword and default New tab

Every call below is valid. Work out which values go to which parameter before you read the output.

Sign in to run this example in your browser.

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=True
The mutable default trap and the None fix New tab

The first function shares one list between calls. The second starts fresh each time. Look at the stored defaults at the end of each section.

Sign in to run this example in your browser.

Expected output

['apple']
['apple', 'pear']
stored default: (['apple', 'pear'],)
['apple']
['pear']
stored default: (None,)
How a call is matcheddiagram
Rendering diagram…
A realistic case: building an API request New tab

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.

Sign in to run this example in your browser.

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.

main.pywhole filepython
def make_tea(cups=1, sugar=0, milk=False):
    return f"{cups} cup(s), {sugar} sugar, milk={milk}"


print(make_tea(sugar=1, 2))
terminal
$ python main.py
── what you'll see ──
File "main.py", line 5
print(make_tea(sugar=1, 2))
^
SyntaxError: positional argument follows keyword argument

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) and func.__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() or random.random() as a default: they are evaluated once at definition time, so every call sees the same value.

  • ▸

    Use is None, not if not basket, in the None pattern. not basket is also true for an empty list the caller passed in on purpose, and you'd silently replace it with a new one.

  • ▸

    When None is a meaningful value for a parameter, use a module-level sentinel such as _MISSING = object() and test if value is _MISSING.

  • ▸

    The same trap, and the same cure, exists in classes: a dataclass refuses items: list = [] with a ValueError and asks for field(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 the def runs, because a does not exist yet, or worse, silently picks up an unrelated global named a). Use None and compute it in the body.

Remember this

  1. 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=value and 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. 2

    A default value is written in the def line as cups=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 a SyntaxError), because otherwise Python could not tell which values go where.

  3. 3

    In a call, positional arguments must come before keyword arguments. f(1, y=2) is fine; f(y=2, 1) is a SyntaxError. You also cannot give the same parameter twice, once by position and once by name: Python raises TypeError: ... got multiple values for argument.

  4. 4

    Default values are created once, when the def line runs, not each time the function is called. For numbers, strings and None this 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. 5

    The fix is a habit: use None as the default and create the fresh list inside the body (if basket is None: basket = []). None is the standard "nothing was given" signal, and the check uses is None (Topic 1.4). When None itself would be a valid input, use a private sentinel object instead (Topic 5.10).

  6. 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). Here temperature sets how random the answer is and max_tokens limits 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

01

Explain the mutable default argument trap and how to avoid it.

02

Which orderings of positional and keyword arguments are allowed in a call, and why?

03

Why must parameters with defaults come after parameters without?

04

When would you prefer keyword arguments over positional ones in a call?

Practice

01

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.

02

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.

03

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. None plus an if is safe for mutable values but adds two lines. For immutable defaults (numbers, strings, booleans) use the real value; it is clearer and shows in help().

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.