Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner Python 3.8+ ~24 min· topic 4 of 10

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=.

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

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

03Keyword-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.

main.pywhole filepython
def total(*numbers, start=0):
    return start + sum(numbers)


print(total(1, 2, 3))              # 6
print(total(1, 2, 3, start=100))   # 106

04Positional-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).

main.pywhole filepython
def greet(name, /, greeting="Hello"):
    return f"{greeting}, {name}!"


print(greet("Asha"))
print(greet("Asha", greeting="Hi"))
# greet(name="Asha")   -> TypeError

05Reading 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).

Reading built-in signaturesdiagram
Rendering diagram…

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. 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. 2

    Inspect more built-ins

    Add print(inspect.signature(round)) and print(inspect.signature(abs)). Which of the two accepts its argument by name? (Some built-ins, such as max, have no signature that inspect can read at all: it raises ValueError. Then the documentation is the only guide.)

  3. 3

    Rename safely

    Rename the parameter name in greet to person (inside the body too). The calls still work, because nobody could name it from outside. Now remove the / and call greet(name="Asha") before and after the rename to see the breaking change.

Code & diagrams

Keyword-only parameters New tab

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.

Sign in to run this example in your browser.

Expected output

http://example.com:80
https://example.com:8080
TypeError: connect() takes 1 positional argument but 2 were given
6
106
Positional-only parameters and built-in signatures Python 3.8+ New tab

The / 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).

Sign in to run this example in your browser.

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')
Three regions in one signaturepython
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-only

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

Pass a positional-only parameter by name

Call greet(name="Asha") where name comes before the /.

main.pywhole filepython
def greet(name, /, greeting="Hello"):
    return f"{greeting}, {name}!"


print(greet(name="Asha"))
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 5, in <module>
print(greet(name="Asha"))
~~~~~^^^^^^^^^^^^^
TypeError: greet() got some positional-only arguments passed as keyword arguments: 'name'

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 as len. Before 3.8 the convention was to prefix parameter names with underscores or to use *args and 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].kind tells 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. b is simply required. Before the *, the old rule still holds, and the / region obeys it too (def f(a=1, /, b) is a SyntaxError).

  • ▸

    Positional-only parameters are an API-evolution tool. A library that ships def parse(text, /) may later rename text or 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. 1

    By default a parameter can be passed either way: def f(a, b) accepts f(1, 2) and f(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. 2

    A keyword-only parameter must be passed by name. def connect(host, *, port=80) accepts connect("a.com", port=8080) but rejects connect("a.com", 8080) with a TypeError. A bare * collects nothing; it just starts the keyword-only region. Writing *args does the same job and also collects extra positionals (def total(*numbers, start=0)).

  3. 3

    Keyword-only parameters make calls readable and prevent argument-swap bugs: connect("a.com", 8080, True) hides what 8080 and True mean, but connect("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. 4

    A positional-only parameter cannot be passed by name. def greet(name, /, greeting="Hello") accepts greet("Asha") but greet(name="Asha") is a TypeError. Many built-in functions work this way: len(obj) has no usable parameter name, so you cannot write len(obj="abc"). Positional-only syntax arrived in Python 3.8; before that only built-ins could behave like this.

  5. 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 called a).

  6. 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 in help() and in inspect.signature. In AI SDKs, nearly every option is keyword-only, which is why SDK calls are always written with names.

Explain it without notes

01

What do * and / mean in a parameter list?

02

Give two reasons to make a parameter positional-only.

03

Why are boolean flags usually best as keyword-only?

04

What is the full left-to-right order of parameter kinds in a signature?

Practice

01

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.

02

Write half(x, /) that returns x / 2. Show that half(x=10) raises TypeError.

03

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 TypeError messages and fix the call.