backend / python core / tricky questions / 33_positional_only_parameters.md

Positional-only and keyword-only parameters (/ and *)

3 min read source

Positional-only and keyword-only parameters (/ and *)

The gotcha

def f(a, *, b) and def f(a, /, b) look similar but mean opposite things. * makes everything after keyword-only; / makes everything before positional-only. Mixing them up changes which calls are valid.

Minimal repro

def keyword_only(a, *, b):
    return a, b

keyword_only(1, 2)           # TypeError: takes 1 positional arg but 2 were given
keyword_only(1, b=2)         #
keyword_only(a=1, b=2)       #

def positional_only(a, /, b):
    return a, b

positional_only(1, 2)        #
positional_only(1, b=2)      #
positional_only(a=1, b=2)    # TypeError: 'a' is positional-only

def both(a, /, b, *, c):
    return a, b, c

both(1, 2, c=3)              #
both(1, b=2, c=3)            #
both(a=1, b=2, c=3)          # TypeError: 'a' is positional-only
both(1, 2, 3)                # TypeError: 'c' is keyword-only

The three regions

def f(positional_only, /, normal, *, keyword_only):
    pass
Position What’s allowed
Before / positional-only — caller cannot use name=value
Between / and * positional-or-keyword (the default) — either works
After * keyword-only — caller must use name=value

Both / and * are markers, not real parameters. Either or both can be omitted.

Why keyword-only (*)

Force callers to use named arguments for clarity:

def fetch(url, *, timeout=10, retries=3, follow_redirects=True):
    ...

fetch("https://...", 30, 5, False)   # unreadable
fetch("https://...", timeout=30, retries=5, follow_redirects=False)   #

This is the more common of the two. Use * for any function with multiple boolean / number flags — readability at the call site dominates.

A bare * (no *args) is the marker:

def f(a, b, *, c, d): ...      # c, d are keyword-only; no var args
def f(a, b, *args, c, d): ...  # c, d keyword-only after *args

Why positional-only (/)

Three reasons, in order of importance:

  1. API stability — change a parameter’s name without breaking callers.
  2. Match C built-inslen(obj), min(a, b), dict(other) — these are positional-only because they always have been; len(obj=x) is a TypeError.
  3. Avoid **kwargs clashes — when **kwargs is part of your signature, / lets you name a parameter the same as a kwarg key without conflict.
def merge(default, /, **overrides):
    # 'default' could collide with overrides if it were positional-or-keyword
    return {**default, **overrides}

merge({"a": 1}, default={"b": 2})    # — 'default' kwarg goes into overrides

Real-world examples

min / max are positional-only (mostly):

min(1, 2, 3)             #
min(iterable=[1, 2, 3])  # TypeError — `iterable` is positional-only

# But min() does accept a `key` kwarg:
min([1, -2, 3], key=abs) # — positional-only / keyword-only mix

dict.update:

{"a": 1}.update(other={"b": 2})    # treats 'other' as a key, not a parameter name
# Implementation: def update(self, /, *args, **kwargs)

functools.partial:

import functools
def divide(num, denom): return num / denom
double = functools.partial(divide, denom=2)
double(num=10)           #

If divide were def divide(num, /, denom), the denom=2 partial application would be safe (no risk of caller passing num=... and conflicting).

Keyword-only with default

The most common real use:

def request(url, *, method="GET", timeout=10, headers=None):
    ...

The first parameter is positional (concise call), the rest must be named. Pretty much every modern Python library uses this pattern for its main API.

When to add either

Situation Use
Function has 3+ parameters * keyword-only for everything past the obvious main one
Boolean flags always keyword-only — func(x, True, False) is unreadable
Two parameters that would be confused at the call site keyword-only
Library API where you want freedom to rename positional-only for the main arg
**kwargs overlaps with a real parameter name positional-only for the parameter

For application code, you’ll use * (keyword-only) constantly and / (positional-only) rarely.

Version notes

  • Keyword-only * — Python 3.0+ (PEP 3102).
  • Positional-only / — Python 3.8+ (PEP 570). Before 3.8 it was achievable only via C extensions or weird **kwargs introspection.
  • inspect.signature shows both correctly.

Interview angle

  • Q: “What’s the difference between def f(a, *, b) and def f(a, /, b)?” — * makes b keyword-only; / makes a positional-only.
  • Q: “When would you use * to mark keyword-only?” — readability for boolean flags / multi-arg functions; force callers to name arguments.
  • Follow-up: “When would you use /?” — API stability (rename without breaking callers), match C built-ins, avoid name clashes with **kwargs.
  • Follow-up: “What does def f(a, b, /, c, *, d) mean?” — a, b positional-only; c positional-or-keyword; d keyword-only.

See 19_starred_unpacking.md, 13_args_kwargs.md.