Skip to content

← Pattern Matching, f-Strings and Newer Syntax step 16 of 23

Medium Primitives

Make Arguments Positional-Only or Keyword-Only

What does this call do?

clamp(2.0, 0.0, 1.0)

You have to open clamp to learn which number is the value and which are the bounds. A signature can settle that for every caller:

def clamp(value: float, /, *, low: float = 0.0, high: float = 1.0) -> float:
    return max(low, min(high, value))

clamp(2.0, high=1.0)   # 1.0
clamp(2.0, 0.0, 1.0)   # TypeError, low and high must be named
clamp(value=2.0)       # TypeError, value cannot be named
  • Parameters before / are positional-only. Callers pass them by position, never by name.
  • Parameters after a bare * are keyword-only. Callers must name them, as in high=1.0.
  • Parameters between the two markers accept either form.

Keyword-only parameters make each call say which value is which. Positional-only parameters let you rename a parameter later without breaking any caller, because no caller can use its name.

Leave a name free for **attrs

A registry takes any attributes as keyword arguments. With def register(self, name, **attrs), the call registry.register("widget", name="label") fails with TypeError: ... got multiple values for argument 'name'. Put a / after name, and name="label" goes into attrs instead. self comes before the / too, so an attribute called self also works.

Let each class name its own parameter

The starter defines a Serialiser protocol. A Protocol lists the methods a class must have. Any class with matching methods fits it, without inheriting from it.

class Serialiser(Protocol):
    def dumps(self, payload: str, /) -> str: ...

The / means no caller can write dumps(payload=...), and mypy rejects any call that tries. So Upper can call the parameter text, Reversed can call it chunk, and every call still works. Without the /, a caller could write payload=, and that call would fail at runtime on Upper, with no warning from mypy.

What to write

  • Upper.dumps(text) returns text.upper().
  • Reversed.dumps(chunk) returns chunk[::-1].
  • In solve, put an Upper and a Reversed in a list[Serialiser], so mypy checks that both classes fit the protocol.
  • clamp(value, /, *, low=0.0, high=1.0) returns max(low, min(high, value)). When low is greater than high, that returns low. Keep it that way, and don't swap the bounds or raise.
  • Registry.register(self, name, /, **attrs: str) stores a copy of attrs under name, replacing any earlier entry. snapshot() returns a new dictionary with a copy of each entry, so changing a snapshot never changes the registry.
  • call_outcome(fn, *args, **kwargs) calls fn once. It returns "TypeError" if the call raises TypeError, and "ok" otherwise. Let any other exception reach the caller.

Build the report in solve

Make a new Registry and register name with attrs. Return:

Key Value
clamped clamp(value, low=low, high=high)
positional call_outcome(clamp, value, low, high)
keyword call_outcome(clamp, value=value)
serialised Two lists, the Upper results for every payload and then the Reversed results
registry The registry snapshot

Both call_outcome calls must return "TypeError". For payloads ["ab", "c"], serialised is [["AB", "C"], ["ba", "c"]], and no payloads give [[], []]. An empty name and an empty attrs are valid.

Why the wrong calls go through call_outcome

Your submission must pass mypy --strict, and mypy rejects a direct clamp(value, low, high) with "Too many positional arguments". call_outcome takes Callable[..., object], and the ... tells mypy to accept any arguments. So the wrong call passes the type check, and Python raises the TypeError when it runs.