Skip to main content

Protocols, overloads, ParamSpec

advanced24 min readLesson 118 of 169

Structure over inheritance; input-dependent outputs; signatures that survive decorators.

Protocols, overloads, and callables

typing.Protocol defines structure, not inheritance. Any object with the right members satisfies the protocol โ€” duck typing that a checker can verify:

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> str: ...

def render(x: Drawable) -> str:
    return x.draw()

class Circle:                      # no inheritance from Drawable
    def draw(self) -> str:
        return "circle"

render(Circle()) type-checks even though Circle never mentions Drawable. @runtime_checkable additionally allows isinstance checks (method presence only, not signatures).

@overload describes input-dependent output types. The implementations carry the body; the overloads are the contract:

from typing import overload, Union

@overload
def parse(value: str) -> list[str]: ...
@overload
def parse(value: bytes) -> str: ...
def parse(value):
    if isinstance(value, bytes):
        return value.decode()
    return value.split(",")

Callables get typed with Callable[[ArgTypes], ReturnType]. For anything richer โ€” keyword args, generics, decorated functions โ€” use ParamSpec:

from typing import TypeVar, ParamSpec, Callable
P = ParamSpec("P")
R = TypeVar("R")

def logged(fn: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("calling", fn.__name__)
        return fn(*args, **kwargs)
    return wrapper

ParamSpec preserves the entire signature through the decorator โ€” the decorated function stays as callable as the original, to the checker and to inspect.signature (via functools.wraps).

Now practice

Protocols & Overloads PracticeStructure-based contracts and input-dependent output types.1 challenge ยท ยท ~20 min