Topic 5.4
Keyword-Only and Positional-Only Parameters
In one line
A bare * in a signature makes every parameter after it keyword-only, and a / makes every parameter before it positional-only (/ arrived in Python 3.8). They let a function's author decide how callers may pass each argument, which keeps APIs readable and flexible to change.
Think of it like this
A hotel booking form. "Guest name" must come first and is written in the first box (you don't write "first box = Asha"). But "smoking room: yes/no" must be labelled, because a bare "yes" in a box would be easy to mix up. Python lets a function's author decide, parameter by parameter, whether callers must write by place, must write by label, or may choose.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Keyword-only parameter
- A parameter that can only be passed by name. It comes after a bare
*or after*args. - Positional-only parameter
- A parameter that can only be passed by position. It comes before a
/. - Bare *
- A
*with no name. It just marks where the keyword-only parameters start. - / (slash)
- A marker in the parameter list. Everything before it is positional-only.
- API
- The set of functions and options a piece of code offers to other code.
- inspect.signature
- A standard-library function that shows a function's parameter list as text.
- Breaking change
- An edit to a function that makes existing callers fail.
Step by step
01Default: either way
A plain parameter can be passed by position or by name. That freedom is convenient, and it also means the parameter's name is part of the function's public contract (what the function promises to its callers): renaming host to server would break every caller who wrote host=.
def connect(host, port):
return f"{host}:{port}"
print(connect("a.com", 80))
print(connect(port=80, host="a.com"))02Keyword-only after a *
Everything after the bare * must be named in the call. Giving a value by position raises a TypeError. host before the star can still go either way.
def connect(host, *, port=80, secure=False):
scheme = "https" if secure else "http"
return f"{scheme}://{host}:{port}"
print(connect("example.com"))
print(connect("example.com", port=8080, secure=True))
# connect("example.com", 8080) -> TypeError03Keyword-only after *args
When a function already collects *numbers, every parameter after it is automatically keyword-only: extra positionals would be swallowed by *numbers anyway.
def total(*numbers, start=0):
return start + sum(numbers)
print(total(1, 2, 3)) # 6
print(total(1, 2, 3, start=100)) # 10604Positional-only before a /
Everything before the / can only be passed by position. This is a Python 3.8 feature. Parameters after the slash can still be passed either way (here greeting).
def greet(name, /, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Asha"))
print(greet("Asha", greeting="Hi"))
# greet(name="Asha") -> TypeError05Reading built-in signatures
Python's own functions use these marks. inspect.signature(len) shows (obj, /), which tells you len takes its argument by position only. sorted shows (iterable, /, *, key=None, reverse=False): the data is positional-only, and key and reverse must be named. That is why you always write sorted(words, key=len) and never sorted(words, len).
06Choosing for your own functions
A good rule: first one or two obvious inputs positional (or positional-only if the name adds nothing), flags and options keyword-only. This keeps call sites readable and gives you room to rename or add options later without breaking anyone.
Try it yourself
- 1
Break the rule on purpose
In the first example, call
connect("example.com", 8080)without the try/except and read the full error. Then change the signature by deleting the*,and run it again: the same call now works, and the meaning of 8080 is unclear. - 2
Inspect more built-ins
Add
print(inspect.signature(round))andprint(inspect.signature(abs)). Which of the two accepts its argument by name? (Some built-ins, such asmax, have no signature thatinspectcan read at all: it raisesValueError. Then the documentation is the only guide.) - 3
Rename safely
Rename the parameter
nameingreettoperson(inside the body too). The calls still work, because nobody could name it from outside. Now remove the/and callgreet(name="Asha")before and after the rename to see the breaking change.
Code & diagrams
The failing call is caught and its error is printed. `try` runs a risky line, and if it fails, the `except` part runs instead of stopping the program (Topic 7.1 teaches it). Read the message: it says one positional argument was allowed.
Expected output
http://example.com:80
https://example.com:8080
TypeError: connect() takes 1 positional argument but 2 were given
6
106The / syntax needs Python 3.8 or newer. inspect.signature shows the marks on real built-ins. `import inspect` loads a toolbox that comes with Python (Topic 6.1 teaches import). The try/except lines catch an error so it can be printed (Topic 7.1).
Expected output
Hello, Asha!
Hi, Asha!
Hey, Asha!
TypeError: greet() got some positional-only arguments passed as keyword arguments: 'name'
len : (obj, /)
sorted : (iterable, /, *, key=None, reverse=False)
greet : (name, /, greeting='Hello')def f(a, b, /, c, d, *, e, g=0):
return (a, b, c, d, e, g)
# f(1, 2, 3, 4, e=5) works
# f(1, 2, c=3, d=4, e=5) works
# f(a=1, b=2, c=3, d=4, e=5) TypeError: a and b are positional-only
# f(1, 2, 3, 4, 5) TypeError: e is keyword-onlyBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Pass a positional-only parameter by name
Call greet(name="Asha") where name comes before the /.
def greet(name, /, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet(name="Asha"))Myth vs fact
Myth
A bare * takes arguments.
Fact
It collects nothing. It is only a marker that says "everything after me is keyword-only".
Myth
Positional-only parameters are only for built-in functions.
Fact
Since Python 3.8 you can write them in your own functions with /.
Myth
Keyword-only parameters are a style choice that Python ignores.
Fact
Python enforces them. A wrong call is a TypeError.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The
/marker (PEP 570) was added mainly so that pure-Python code could imitate C functions such aslen. Before 3.8 the convention was to prefix parameter names with underscores or to use*argsand unpack by hand. - ▸
Making an option keyword-only is the cheapest insurance for an API: you can add new options and reorder them without breaking old calls, because no call ever depended on their position.
- ▸
inspect.signature(func).parameters[name].kindtells you each parameter's kind (POSITIONAL_ONLY,POSITIONAL_OR_KEYWORD,VAR_POSITIONAL,KEYWORD_ONLY,VAR_KEYWORD). Libraries such as Pydantic and tool-schema generators read it to know how a function may be called. - ▸
The "defaults must come last" rule (Topic 5.2) does not apply after a
*:def f(*, a=1, b)is legal, because keyword-only parameters are matched by name, never by place.bis simply required. Before the*, the old rule still holds, and the/region obeys it too (def f(a=1, /, b)is aSyntaxError). - ▸
Positional-only parameters are an API-evolution tool. A library that ships
def parse(text, /)may later renametextor widen the function, with no call breaking. Without the/, every parameter name is public contract, and changing it is a breaking change that only shows up in your users' code. This is why many standard-library functions, including most built-ins, show a/in their signatures.
Remember this
- 1
By default a parameter can be passed either way:
def f(a, b)acceptsf(1, 2)andf(a=1, b=2). Two special marks in the parameter list narrow that. The mark/means "everything before me is positional-only". The mark*means "everything after me is keyword-only". - 2
A keyword-only parameter must be passed by name.
def connect(host, *, port=80)acceptsconnect("a.com", port=8080)but rejectsconnect("a.com", 8080)with aTypeError. A bare*collects nothing; it just starts the keyword-only region. Writing*argsdoes the same job and also collects extra positionals (def total(*numbers, start=0)). - 3
Keyword-only parameters make calls readable and prevent argument-swap bugs:
connect("a.com", 8080, True)hides what 8080 and True mean, butconnect("a.com", port=8080, secure=True)does not. They are the right choice for booleans, flags, timeouts and any option a reader would not guess. - 4
A positional-only parameter cannot be passed by name.
def greet(name, /, greeting="Hello")acceptsgreet("Asha")butgreet(name="Asha")is aTypeError. Many built-in functions work this way:len(obj)has no usable parameter name, so you cannot writelen(obj="abc"). Positional-only syntax arrived in Python 3.8; before that only built-ins could behave like this. - 5
Why would an author want it? (1) When the parameter name carries no meaning (
len,abs). (2) So the author can later rename the parameter without breaking callers who never used the name. (3) So the name can be used again in**kwargs(def f(a, /, **kwargs)lets callers pass a keyword also calleda). - 6
You can combine everything:
def f(a, b, /, c, d, *, e, f). Parameters before/are positional-only, between/and*can go either way, and after*are keyword-only. You can read a signature this way inhelp()and ininspect.signature. In AI SDKs, nearly every option is keyword-only, which is why SDK calls are always written with names.
Explain it without notes
What do * and / mean in a parameter list?
Give two reasons to make a parameter positional-only.
Why are boolean flags usually best as keyword-only?
What is the full left-to-right order of parameter kinds in a signature?
Practice
Write describe(name, *, age=0, city="unknown") that returns text like Asha, 12, Pune. Show a valid call, and show that passing the age positionally raises TypeError.
Write half(x, /) that returns x / 2. Show that half(x=10) raises TypeError.
Use inspect.signature to print the signatures of round and len. Which of the two lets you pass its first argument by name?
Trade-offs
- ↔
Strictness versus convenience. Keyword-only parameters make callers type a little more and make mistakes impossible. For private helper functions used in two places, the extra ceremony may not be worth it.
- ↔
Positional-only versus a good name. If the parameter name is useful to callers (
timeout=), keep it normal. Use/when the name would add nothing or you want freedom to rename.
Done when you can
Done when you can write a function with a keyword-only parameter after a bare
*.Done when you can write a positional-only parameter with
/and tag it Python 3.8.Done when you can read
(iterable, /, *, key=None, reverse=False)and say how to call it.Done when you can explain why an API author would use either mark.
Done when you can read both
TypeErrormessages and fix the call.