Positional, keyword and default arguments in Python are the three ways a value reaches a function, and CPython binds them in a strict, documented order. Learn that order and the confusing TypeError messages become predictable.
How does Python bind arguments to parameters?
When you call a function, Python matches the values you supplied to the parameters you declared in a fixed order of operations. Positional values are consumed first, filling parameters from left to right. Keyword values are matched by name, in whatever order you wrote them. Any parameter still unfilled afterwards falls back to its default value. If a parameter has no default and is still unfilled, the call fails immediately with TypeError: missing 1 required positional argument.
def make_label(prefix, item, quantity=1):
return f"{prefix}{item} x{quantity}"
print(make_label("Box ", "tea"))
print(make_label("Box ", "tea", 3))
print(make_label(item="tea", prefix="Box "))
print(make_label("Box ", item="tea", quantity=2))
Notice the fourth call. The positional value fills prefix, the keyword value fills item, and the default fills quantity. Mixing the styles is completely legal as long as every positional value comes before every keyword value in the call.
Parameters in a definition are not all alike. Python recognises five kinds, and knowing which kind you are looking at tells you how a caller may pass it.
| Parameter kind | Written as | How callers may pass it |
|---|---|---|
| Positional-only | def f(a, /) | By position only; the name is invisible to callers |
| Positional-or-keyword | def f(a) | By position or by name |
| Keyword-only | def f(*, a) | By name only |
| Var-positional | def f(*args) | Any number of extra positional values |
| Var-keyword | def f(**kwargs) | Any number of extra keyword values |
What is the difference between positional and keyword arguments?
Positional arguments rely on order. Swap two of them and the function still runs, but it now means something different, which is why positional calls are fragile the moment a signature has more than two parameters. Keyword arguments rely on names, so order stops mattering and the call site documents itself.
def connect(host, port, timeout, retries):
return f"{host}:{port} timeout={timeout} retries={retries}"
print(connect("db.internal", 5432, 30, 3))
print(connect(host="db.internal", port=5432, timeout=30, retries=3))
print(connect("db.internal", port=5432, timeout=30, retries=3))
The third line is the one worth memorising: positional values must come first, keyword values after. Writing connect(port=5432, "db.internal") is a SyntaxError, because Python cannot tell which parameter the second positional value was meant for once you started naming things explicitly.
There is a real cost to keyword arguments too, and it shows up in hot loops: building a keyword call means Python assembles a dictionary for the call, while a plain positional call skips that work. For a function called millions of times, positional arguments are measurably cheaper. For a function called a handful of times, the clarity of connect(host=..., port=...) is worth far more. Decide per call site rather than by dogma.
Supplying the same parameter twice is also an error, and the message names the culprit. Note that the two failure modes differ: the compiler rejects a repeated keyword, while the interpreter rejects a parameter that received both a positional value and a keyword value.
def connect(host, port):
return f"{host}:{port}"
try:
connect("db.internal", 5432, port=3306)
except TypeError as exc:
print("TypeError:", exc)
source = "connect(host='a', port=1, port=2)"
try:
compile(source, "<demo>", "exec")
except SyntaxError as exc:
print("SyntaxError:", exc.msg)
When should you use default arguments?
A default says "this parameter is optional, and here is what it means when the caller says nothing". That is exactly the right tool for configuration knobs such as a timeout, a page size or a separator. It is the wrong tool for data the function genuinely cannot work without, because a default silently invents a value that may be nonsense for the caller's data.
Python enforces one ordering rule in a definition: a parameter without a default cannot follow one with a default. The compiler rejects it at parse time, long before your program runs.
source = "def broken(a=1, b): pass"
try:
compile(source, "<demo>", "exec")
except SyntaxError as exc:
print("SyntaxError:", exc.msg)
When a signature mixes required and optional parameters, keep the required ones first. If that is awkward, make the optional ones keyword-only and the ordering problem disappears entirely.
Defaults also change what a function's signature promises to a type checker. With def scale(value, factor=2), a checker knows scale(x) returns the same type as scale(x, 2). With def scale(value, factor=None), the checker has no idea what None turns into, so you are forced to handle the sentinel yourself. Defaults are part of the interface, and the interface should tell the truth about what the function does with the value it was given.
Why are mutable default arguments a trap?
Default values are evaluated once, when the def statement runs, not on every call. A default of [] therefore creates one list that every call to the function shares. Append to it and the next caller inherits your leftovers.
def collect(item, bucket=[]):
bucket.append(item)
return bucket
print(collect("apple"))
print(collect("pear"))
That is the classic symptom of a shared default: the second call shows a list containing both items, even though the caller only ever passed a pear. The fix is to use None as the sentinel and build the real default inside the body.
def collect(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(collect("apple"))
print(collect("pear"))
Immutable defaults such as 0, "" or None are safe, because the caller cannot change them in place. The trap only bites when the default is a list, a dict or a set.
How do positional-only and keyword-only parameters work?
A forward slash in the parameter list marks every parameter to its left as positional-only. A lone asterisk marks every parameter to its right as keyword-only. Both have been available since Python 3.8, and both exist to protect you from yourself.
def send(sender, recipient, /, *, subject, body="", urgent=False):
tag = "URGENT " if urgent else ""
return f"{tag}from {sender} to {recipient}: {subject} {body}".strip()
print(send("[email protected]", "[email protected]", subject="Welcome"))
print(send("[email protected]", "[email protected]",
subject="Deadline", body="Submit by Friday", urgent=True))
Marking sender and recipient positional-only prevents a caller from writing sender="x", recipient="y", which sounds friendlier but breaks the moment you rename a parameter. Marking the rest keyword-only means the order of subject, body and urgent can never be misremembered, and it lets you add a new keyword-only parameter without breaking existing calls.
Positional-only parameters are also the right choice when a parameter name is a poor description. If your function has two mode parameters, the name alone cannot disambiguate the call, so you make them positional-only and let the position carry the meaning. The standard library does this constantly: str.replace(old, new, count) would be confusing if you could write replace(new="x").
What to remember
The error messages are precise if you read them properly, so keep this table next to you while debugging.
| Error message | What you did | Fix |
|---|---|---|
missing 1 required positional argument | Forgot a parameter that has no default | Pass it, or give it a default |
got multiple values for argument 'x' | Supplied the same parameter twice | Remove the duplicate |
takes from 1 to 2 positional arguments but 3 were given | Passed too many positional values | Drop the extra or accept it with *args |
parameter without a default follows parameter with a default | Wrote the signature in the wrong order | Reorder, or make the later ones keyword-only |
got an unexpected keyword argument 'x' | Typo, or the parameter is positional-only | Check the spelling and the parameter kind |
Try it yourself: build a schedule_job function with name and hour as required positional parameters, minute=0 and recurring=False as keyword-only parameters with defaults, and a */-free signature that still forces minute and recurring to be named. Call it five ways, including one that triggers a TypeError, and paste the error text into a comment. Then write a second version with a mutable default, call it twice, and explain in a comment why the second result differs from the first.