We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modern Syntax and Modernisation step 16 of 18
Positional-only `/` and keyword-only `*`
The moment a parameter name is reachable by keyword, it is public API. Renaming it is a breaking change — even when the rename is a pure clarity improvement, even when nobody in your repo uses it by keyword, because you do not control every caller. PEP 570 gave pure-Python functions the marker that C functions always had.
def clamp(value: float, /, *, low: float = 0.0, high: float = 1.0) -> float:
Everything before / is positional-only. Everything after * is
keyword-only. The middle is both.
The four motivations, and the one you will actually feel
-
Rename freedom.
valueabove can becomexin the next release without breaking anyone. -
`kwargs
collision avoidance.** This is why the stdlib writesdict.update(self, /, other, **kwargs): without the/, a caller could never store a key calledselforother`. - C-accelerator parity. A Python fallback with keyword-capable parameters accepts calls the C version rejects — a difference that only shows up on the platform without the accelerator.
- Speed. Marginal, and not a reason on its own.
The * side solves a different problem: the boolean trap. send(msg, True, False) is unreadable and unreviewable; send(msg, urgent=True, retry=False)
is not. Making flags keyword-only is the cheapest readability win in API
design, and unlike a lint rule it is enforced by the interpreter.
Protocols: why / is not optional
class Serialiser(Protocol):
def dumps(self, payload: str, /) -> str: ...
Without the /, a conforming class must name its parameter payload —
structural typing would be quietly enforcing a naming convention. With it, both
of these conform:
class Upper:
def dumps(self, text: str, /) -> str: ...
class Reversed:
def dumps(self, chunk: str, /) -> str: ...
The same asymmetry is why Callable[[int, str], None] describes
positional-only parameters. A function with named parameters is not
assignable to it in the strictest reading — and it is why most dunder overrides
(__eq__, __format__, __contains__, __getitem__) need a / to satisfy
--strict.
What you are building
-
UpperandReversed, both satisfying the givenSerialiserProtocol while naming their parameter differently. Assigning them to alist[Serialiser]is what proves it under--strict. -
clamp(value, /, *, low=0.0, high=1.0)—max(low, min(high, value)). -
Registry.register(self, name, /, **attrs: str)andsnapshot(). -
call_outcome(fn, *args, **kwargs) -> str—"TypeError"if the call raisesTypeError, else"ok". Note the parameter type:Callable[..., object]. -
solve(...)returningclamped,positional(the outcome ofclamp(value, low, high)),keyword(the outcome ofclamp(value=value)),serialised(one list per serialiser) andregistry.
Both probes must report "TypeError". If either says "ok", your signature is
wrong — and the test data includes an attribute literally named name, which
only works because register‘s first parameter is unreachable by keyword.
Why the probes need Callable[..., object]
clamp(value, low, high) written literally is a static error — mypy stops
you before the runtime ever sees it, which is the good outcome and also makes
it untestable. Routing the call through a Callable[..., object] erases the
parameter list, so the call compiles and the TypeError is observable. That
erasure is not a trick invented for this exercise; it is exactly what happens
every time you store a handler in a dict[str, Callable[..., Any]], and it is
worth knowing that you have turned the gate off when you do.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.