Skip to content

Callable protocol

Problem

collections.abc.Callable (formerly typing.Callable) lets you specify the type of a callable object:

def stringify_points(
    points: Iterable[Point],
    fn: Callable[[int, int], str],
) -> str:
    return "\n".join(fn(p.x, p.y) for p in points)

However, Callable doesn't cover all cases when you need a callable.

  • With more than one parameter, it's often unclear what the two parameters stand for. What do you do with Callable[[int, str], None]?
  • Callable only lets you specify positional-only parameters
  • Callable can't describe generic functions (functions that use TypeVars)
  • Callable doesn't support overloaded functions

Solution

The solution to all these problems is to define a Callable Protocol. It's a protocol class that defines a __call__ method and nothing else.

from typing import Protocol


# keyword-able arguments
class PointCallback(Protocol):
    def __call__(self, x: int, y: int) -> None:
        ...


# overloads (note that the usual 'implementation' declaration is not needed)
class WeirdCallback(Protocol):
    @overload
    def __call__(self, thing: int) -> None:
        ...

    @overload
    def __call__(self, thing: int, thong: str) -> str:
        ...

Comparison with Callable

Pros

  • More flexible than the Callable syntax
  • Even if you don't need the bells and whistles, naming arguments helps make your API more self-explanatory

Cons

  • More verbose. It simply takes up more space than a simple Callable[...]
  • Can't be anonymous, unlike a Callable
  • Requires users to be familiar with Protocols
  • When hovering over an element using a protocol as annotation, you might have to jump through some hoops to get to its definition

Applicability

If you need some features a Callable[...] doesn't support, such as keyword arguments, this is probably the way to go.

If you need to clarify the purpose of each parameter of a callback with names, consider using this pattern.

If you need a very simple callback with one or two parameters, you likely don't need this pattern.