pythonpython-typingcallable

Differences between Protocol.__call__ vs Callable?


I have a function

def run_thing(cb: Callback) -> Result:
  w: Widget = make_widget()
  res: Result = cb(w)
  return res

I can imagine two ways to define the Callback type.

# Option 1: Callable
# https://docs.python.org/3/library/typing.html#annotating-callables
Callback = typing.Callable[[Widget],Result]
# Option 2: Protocol
# https://docs.python.org/3/library/typing.html#typing.Protocol
class Callback(typing.Protocol):
  def __call__(self,w: Widget) -> Result:
    ...

What are the nuances to consider in the decision between the two above type definitions?


Solution

  • Callable can be used for simple cases, but will be too limited for complex signatures.

    This is explained by the documentation that also suggests to define a Protocol as an alternative with the following quote and example snippet :

    Callable cannot express complex signatures such as functions that take a variadic number of arguments, overloaded functions, or functions that have keyword-only parameters. However, these signatures can be expressed by defining a Protocol class with a __call__() method:

    --- Python's typing documentation

    from collections.abc import Iterable
    from typing import Protocol
    
    class Combiner(Protocol):
        def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: ...
    
    def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes:
        for item in data:
            ...
    
    def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]:
        ...
    def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]:
        ...
    
    batch_proc([], good_cb)  # OK
    batch_proc([], bad_cb)   # Error! Argument 2 has incompatible type because of
                             # different name and kind in the callback