skip to content

What does the `/` marker mean in a Python signature like `def f(a, b, /, c)`?

level: middleimportance: should knowfreq 40%

answer

  1. A marker that removes a caller's choice
  2. Only the position, never the name
  3. Names to its left are not API
  4. Slash after the first parameters
  5. PEP 570, Python 3.8, positional-only

basics

~10 s

Every parameter before the / is positional-only: callers may pass it by position but never by name. f(1, 2, 3) works; f(a=1, b=2, c=3) raises TypeError. Python 3.8 added the syntax via PEP 570.

solid answer

~40 s

`/` splits the signature: `a` and `b` are **positional-only** while `c` remains positional-or-keyword. Passing a positional-only parameter by name raises `TypeError: f() got some positional-only arguments passed as keyword arguments: 'a'`. The point is that a parameter name only becomes part of your public API if a caller is allowed to use it — marking the obvious operands positional-only means you can rename them freely later, and it frees those names for other uses inside the signature. PEP 570 added the syntax to Python-level `def` statements in **3.8**; before that only C-implemented callables could behave this way, which is why builtin documentation had shown signatures like `len(obj, /)` for years.

code

python · 8 lines
python
def scale(value, /, factor=2):
    return value * factor

print(scale(3), scale(3, factor=4))
try:
    scale(value=3)
except TypeError as e:
    print(e)

go deeper

for a junior

Recognise the marker when you meet it in documentation such as len(obj, /), and know it means you must pass that argument by position, without naming it.

for a middle

Explain the mechanics and the ordering rules: what the TypeError says, that / must precede any bare *, and that at least one parameter must come before it.

for a senior

Argue the API consequence — a parameter name becomes a public promise the moment callers may use it, and positional-only is how you decline to make that promise.

for a principal

Set the house convention: which classes of parameter are positional-only, which are keyword-only, and how a team introduces either marker without breaking downstream callers silently.

## What the marker declares `/` is a separator, like the bare `*`, not a parameter: it has no name and binds no argument. Every parameter to its left is **positional-only** — the caller may supply it by position and by position only. Parameters to its right keep whatever kind they would otherwise have: positional-or-keyword by default, or keyword-only if a `*` appears later. So in `def f(a, b, /, c)`, `f(1, 2, 3)` and `f(1, 2, c=3)` both work, while `f(1, b=2, c=3)` fails: ``` TypeError: f() got some positional-only arguments passed as keyword arguments: 'b' ``` That message is unmistakable and is worth recognising on sight — it means the signature has a `/` in it and the caller tried to use a name that is not part of the contract. ## Where it may appear At least one parameter must precede it — `def f(/, a)` fails with "at least one argument must precede /" — and it must come before any bare `*`, so `def f(a, *, b, /)` fails with "/ must be ahead of *". The complete legal order is: positional-only parameters, `/`, positional-or-keyword parameters, `*` (or a `*args` collector), keyword-only parameters. A canonical shape combining both markers is `def f(a, /, b=1, *, c)`. ## Why the feature exists The historical half of the answer is that CPython's C-implemented callables have always bound arguments positionally. A function written in C had no ordinary mechanism for keyword matching, so `len(obj)` accepted no `len(obj=x)` form, and the documented signature was written `len(obj, /)` to say so — using a syntax that Python-level code could not itself write. PEP 570 closed that gap in **Python 3.8**: pure-Python functions can now express exactly the same contract, so a Python implementation of a C function is drop-in compatible, and library authors get the same tool the interpreter had. The design half is more interesting, and it is what an interviewer is usually after. **A parameter's name is part of your public API only if callers are permitted to use it.** The moment a caller writes `f(value=3)`, the identifier `value` is a promise: renaming it to `x` in a later release breaks that caller. For parameters that are the obvious subject of the call — the sequence you are sorting, the object you are measuring, the number you are rounding — nobody wants to name them anyway, and the name is pure internal detail. Marking them positional-only says so explicitly and buys back the freedom to rename, to reorder within the positional-only block, or to swap in a different implementation whose internal names differ. There is a second, more mechanical payoff: a name that is positional-only is no longer reserved. A function that forwards arbitrary keyword data to something else can accept a caller-supplied key that happens to collide with its own parameter name, because the parameter can never be filled by name and the keyword therefore has somewhere else to go. ## When to use it and when not to Reach for `/` when the parameter is genuinely anonymous at the call site and you want no name-based coupling: single-operand utilities, the "subject" arguments of a data-manipulation function, anything mirroring a builtin. Do **not** reach for it when the name carries meaning that helps a reader — `timeout`, `encoding`, `strict` should stay nameable, and are usually better off keyword-only. Adding `/` to a published function is a breaking change for any caller who was passing those parameters by name; the compensation is that the break is a loud `TypeError` at the call rather than a silent behavioural difference. As with the bare `*`, the marker is cheapest when it is part of the signature's first version. Deciding this once — obvious operands positional-only, options keyword-only, very little in the middle — turns a recurring argument in code review into a house rule. ## What it is not It does not divide required parameters from optional ones; defaults are orthogonal, and `def f(a, /, b=1)` mixes both. It does not make anything keyword-only — that is the opposite marker. And it is not a runtime performance feature: it constrains how a call may be written, not how it is executed.

  • Why do so many builtin functions show a `/` in their documented signatures?
    Because functions implemented in C have always bound their arguments by position only — there was no keyword matching to fall back on. The documentation needed a way to write that contract, so it borrowed the `/` notation years before PEP 570 made it valid Python syntax in 3.8. `len(obj, /)` and `sorted(iterable, /, *, key=None, reverse=False)` are typical.
  • Where in a parameter list is `/` allowed to appear?
    After at least one parameter and before any bare `*`. `def f(/, a)` fails to compile with 'at least one argument must precede /', and `def f(a, *, b, /)` fails with '/ must be ahead of *'. The full legal order is positional-only, `/`, positional-or-keyword, `*`, keyword-only.
  • Does `/` change anything about default values?
    No — the two are orthogonal. A positional-only parameter may have a default, as in `def f(a, /, b=1)`, and the usual rule still applies inside the positional region: a parameter without a default may not follow one that has a default, so `def f(a=1, /, b)` is a SyntaxError.

A positional-only parameter is an unlabelled slot on a paper form: you fill it by where it sits, and whatever the designer called it in their own notes is none of your business.

saying these in an interview costs you the question

  • Thinks `/` separates required from optional parameters
  • Says positional-only is possible only in C code
  • Believes `/` makes those parameters keyword-only
  • Claims every parameter name is part of the API
  • Confuses `/` with the bare `*` marker
  • Treats passing by name there as a style choice

context