Skip to content

← Interfaces with Protocols and ABCs step 7 of 26

Medium Framework

Type a Handler Registry with a Callback Protocol

A registry maps names to text handlers. Each handler takes a string and an optional keyword argument retries, and returns a string. The starter types the registry as dict[str, Callable[..., Any]]. The ... accepts any arguments and Any turns off checking of the result, so mypy cannot catch a handler with the wrong shape.

Describe the handler with a callback protocol

Callable[[str, int], str] lists positional parameters only. It cannot say that retries is a keyword argument, or that callers may leave it out. A callback protocol can. It is a protocol with a __call__ method, so it describes how you call an object:

class Handler(Protocol):
    def __call__(self, payload: str, /, *, retries: int = 0) -> str: ...

The / makes payload positional-only, and the * makes retries keyword-only. The = 0 tells mypy that callers may leave retries out. A plain function fits Handler when it accepts the same calls, without inheriting from anything.

With dict[str, Handler], mypy rejects a handler that names its keyword retry, one that requires retries, and one that returns bytes.

What to write

Declare Handler.__call__ as above, and change the annotation to REGISTRY: dict[str, Handler]. Keep the three handler signatures from the starter, including /, * and the default of 0, and fill in their bodies:

Key Returns
shout payload.upper() + "!" * retries
tag f"[{retries}]{payload}"
trim payload.strip() * (retries + 1)
shout("ab", retries=2)     # "AB!!"
tag("x")                   # "[0]x"
trim("  hi  ", retries=1)  # "hihi"
shout("z", retries=-1)     # "Z"
trim("x", retries=-1)      # ""

Do not clamp a negative retries. A string times zero or a negative number is "", and the tests expect that. The handlers retry nothing. Here retries only changes the formatting.

dispatch(name, payload, retries) -> str looks up name in REGISTRY. The keys are case-sensitive. If name is missing, return f"unknown:{name}". Otherwise call the handler once, as handler(payload, retries=retries), and return its result. Let any exception from a handler propagate.

solve(names, payloads, retries) -> list[str] walks the three lists together with zip(..., strict=True) and dispatches each triple in order. Three empty lists give [], and lists of different lengths raise ValueError.

solve(["tag", "Shout", "trim"], ["x", "y", " z "], [2, 0, 0])
# ["[2]x", "unknown:Shout", "z"]