datarekha
Python Easy Asked at AmazonAsked at Microsoft

What is the difference between positional and keyword arguments in Python?

The short answer

Positional arguments are matched by their position in the call; keyword arguments are matched by name and can appear in any order. Python 3 also introduced positional-only (/) and keyword-only (*) separators to enforce calling conventions in function signatures.

How to think about it

The question sounds basic but branches into genuinely useful territory: the / and * separators library authors use to keep APIs future-proof, and the mutable-default trap that snags even experienced engineers.

The core is simple. Python matches each argument to a parameter one of two ways — by position (the order you wrote them) or by name (param=value). You can mix both in one call, with the single rule that positionals come before keywords.

A worked example

def train(model_name, n_epochs=10, lr=0.001, verbose=False):
    return f"Training {model_name}: epochs={n_epochs}, lr={lr}, verbose={verbose}"

# All positional — order decides which parameter gets which value
print(train("LinearSVC", 20, 0.01, True))

# Mixed — named args may appear in any order and skip optional ones
print(train("RandomForest", lr=0.05, n_epochs=5))

# All keyword — the most readable form for a long signature
print(train(model_name="XGBoost", n_epochs=50, lr=0.1, verbose=True))

# / makes params before it positional-only; * makes params after it keyword-only
def strict(pos_only, /, normal, *, kw_only):
    return f"pos={pos_only}, normal={normal}, kw={kw_only}"

print(strict(1, 2, kw_only=3))         # ok — pos_only positional, kw_only named
print(strict(1, normal=2, kw_only=3))  # ok — 'normal' may go either way
# strict(pos_only=1, ...) -> TypeError;  strict(1, 2, 3) -> TypeError
Training LinearSVC: epochs=20, lr=0.01, verbose=True
Training RandomForest: epochs=5, lr=0.05, verbose=False
Training XGBoost: epochs=50, lr=0.1, verbose=True
pos=1, normal=2, kw=3
pos=1, normal=2, kw=3

Watch the second train call: it set lr and n_epochs by name, out of order, and left verbose at its default — exactly what keyword arguments buy you, readability and optionality. The strict signature then shows the two separators at work: pos_only must be positional, kw_only must be named, and normal is free to go either way.

Why the separators matter

/ says “everything before me cannot be passed by name.” That lets a library rename a positional parameter later without breaking callers — it’s why len([1,2,3]) works but len(obj=[1,2,3]) raises TypeError. * says “everything after me must be named,” forcing callers to be explicit — invaluable when a function takes several boolean flags that are easy to swap by accident.

The default-value sharp edge

Default values are evaluated once, when def runs — not on each call — which bites when the default is mutable:

# Bug: every call that omits lst shares the SAME list
def append_to(val, lst=[]):
    lst.append(val)
    return lst

# Fix: None sentinel, build the list inside
def append_to(val, lst=None):
    if lst is None:
        lst = []
    lst.append(val)
    return lst
Learn it properly Functions

Keep practising

All Python questions

Explore further

Skip to content