PYTHON / FUNCTIONS
Positional-only parameters
Use the / marker to make leading parameters positional-only, so their names stay private and keyword arguments can safely reuse them.
What you will learn
- Write signatures with / to force leading arguments to be passed by position
- Rename a positional-only parameter without breaking a single caller
- Combine / and * to create positional-only, flexible, and keyword-only zones
- Accept **kwargs keys that collide with a parameter name, without a TypeError
Understanding Positional-only parameters
A plain parameter like the value in def double(value) can be filled two ways: double(3) or double(value=3). Since Python 3.8 (PEP 570) a bare / in the parameter list marks a cut-off: every parameter written before the / can only be filled positionally. Together with the * marker, a signature has up to three zones, in this fixed order: positional-only, then positional-or-keyword, then keyword-only, as in def f(a, b, /, c, *, d).
The point is not extra strictness for its own sake. A parameter name that callers can use in a call becomes part of your public contract, so renaming it later breaks code; putting it before / turns the name back into a local implementation detail. C-implemented builtins have always worked this way, which is why len(obj=[1,2]) fails, and it is what lets dict.update accept a key literally called self. The / marker is how you get that same behaviour in pure Python.
The mental model to keep is the argument-binding order. Positional arguments fill parameter slots left to right; then each keyword argument is looked up by name among the parameters that are still eligible, and anything left over goes to **kwargs or raises TypeError. A / does not add a check after binding, it removes those parameter names from the keyword lookup table entirely, which is exactly why a matching keyword falls through to **kwargs instead of clashing.
def resize(image, width, height, /, keep_ratio=True):
return f"{image} -> {width}x{height} ratio={keep_ratio}"
print(resize("logo.png", 800, 600))
print(resize("logo.png", 800, 600, keep_ratio=False))
try:
resize("logo.png", width=800, height=600)
except TypeError as e:
print("TypeError:", e)A bare / in a parameter list removes the preceding parameters' names from keyword matching, making them fillable by position only.
Worked examples
Freeing a name for **kwargs
Shows how / lets a keyword argument with the same name as a parameter land in **kwargs instead of colliding.
def set_attrs(element, /, **attrs):
return element, attrs
print(set_attrs("div", element="wrapper", id="main"))
def set_attrs_loose(element, **attrs):
return element, attrs
try:
set_attrs_loose("div", element="wrapper")
except TypeError as e:
print("TypeError:", e)Example explained
Line 1element sits before the /, so the name element is not a candidate during keyword matching.
Line 2The keyword element="wrapper" therefore falls through into attrs alongside id="main".
Line 3In set_attrs_loose the name is still eligible, so "div" and "wrapper" both target the same slot.
Line 4That double assignment is what produces "got multiple values for argument".
Inspecting the three zones
Uses inspect to read back the kind of each parameter in a signature that mixes / and *.
import inspect
def transfer(src, dst, /, amount, *, memo=""):
return f"{amount} from {src} to {dst} ({memo!r})"
print(transfer("a", "b", 10, memo="rent"))
print(transfer("a", "b", amount=10))
for p in inspect.signature(transfer).parameters.values():
print(p.name, p.kind.name)Example explained
Line 1src and dst come before the /, so only transfer("a", "b", ...) can fill them.
Line 2amount sits between / and *, so both 10 and amount=10 work for it.
Line 3memo comes after the *, so it can never be passed positionally.
Line 4p.kind.name reports the zone Python recorded at compile time, not something checked per call.
Builtins already do this
Demonstrates that C-level functions have long had positional-only arguments, which is what / mirrors.
print(len([1, 2, 3]))
try:
len(obj=[1, 2, 3])
except TypeError as e:
print("TypeError:", e)
d = {}
d.update(self="oops", other=1)
print(d)Example explained
Line 1len's single argument is positional-only, so obj is not a usable keyword even though the docs name it obj.
Line 2dict.update declares its instance parameter as positional-only, so self is a free name for callers.
Line 3That is why d.update(self="oops") stores a key rather than raising a multiple-values error.
Important notes
The / syntax exists only in Python 3.8 and later; on 3.7 the def line itself is a SyntaxError, not a runtime failure.
A positional-only parameter may still have a default, but overriding it requires passing every parameter before it positionally as well.
Common mistakes
Writing def f(/, a, b) with nothing before the slash: Python raises SyntaxError at compile time, so the whole module fails to import.
Putting the / after the *, as in def f(*args, /): also a SyntaxError, because the zones must appear in the order positional-only, flexible, keyword-only.
Forwarding arguments as a mapping, like f(**{"width": 800}), to a positional-only parameter: the keys are treated as keywords and rejected, so you must unpack a sequence with *args instead.
Try it yourself
Change, predict, then run
Write def slice_text(text, start, /, end=None) that returns text[start:end], then call it once correctly and once with start=0 and print the TypeError message you get.
Open the Python workspaceCheck your understanding
A logging helper is declared def log(message, /, **fields). Why does log("saved", message="row 7") work without error?
- Because message is positional-only, its name is excluded from keyword matching, so message="row 7" is collected into fields
- Because **fields is matched before the named parameters, so it absorbs every keyword argument passed
- Because the keyword value silently overwrites the positional one, leaving message equal to "row 7"
- Because / makes message optional, so Python skips binding it and passes everything through
Show answer
The / removes message from the table of names that keyword arguments can bind to, so the leftover keyword goes to **fields and the parameter keeps the value "saved". Option 2 reverses the real order: named parameters are always matched first, and **kwargs only receives what is left, which is exactly why def log(message, **fields) would instead raise "got multiple values for argument 'message'".