Search code examples
pythonvariadic-functionstyping

Typehint a variadic function in Python


I wrote a higher-order python function (let's call it parent) and its parameter function (let's call it child) is a variadic function.

I don't know how to typehint it.

child takes as parameter a first argument that always is a str and a variable number of parameters that can be anything. It returns Any.

The closest I can get to it is Callable[..., Any] but then I "lose" the fact that the first argument is a str.

I would like something as such Callable[[str,...], Any] but this is not a valid typehint.

Is there a way to typehint my function?


Solution

  • Using a Protocol does not require you to manually wrap values in a "type-hinting wrapper", but unfortunately it doesn't help you here.

    If a function can match a protocol, it's signature must exactly match that of the __call__ method in the protocol. However, (if I'm not mistaken) you want to match any function with a string as the first argument, which can be any of the following:

    def fn1(x: str) -> Any: ...
    def fn2(x: str, arg1: int, arg2: float) -> Any: ...
    def fn3(x: str, *args: Any, **kwargs: Any) -> Any: ...
    

    these all have different signatures, and thus cannot be matched by a single protocol: (mypy-play)

    from typing import Any, Protocol
    
    # This might be the protocol you might use, but unfortunately it doesn't work.
    class StrCallable(Protocol):
      def __call__(self, x: str, *args, **kwargs) -> Any:
        ...
    
    def my_higher_order_fn(fn: StrCallable) -> StrCallable:
      return fn  # fill this with your actual implementation
    
    my_higher_order_fn(fn1)  # fails
    my_higher_order_fn(fn2)  # fails
    my_higher_order_fn(fn3)  # this passes, though
    

    PEP 612 introduced ParamSpec, which is what you'll need here. It's kinda like TypeVar, but for function signatures. You can put a ParamSpec where you'd put the first list argument in Callable:

    from typing import Callable, Concatenate, ParamSpec, TypeVar
    
    P = ParamSpec("P")
    TRet = TypeVar("TRet")
    
    StrCallable = Callable[Concatenate[str, P], TRet]
    

    where Concatenate is concatenating types to an existing parameter spec. Concatenate[str, P] is exactly what you need: any function signature whose first argument is a str.

    Unfortunately, PEP 612 won't be available until Python 3.10, and mypy does not yet fully support it either. Until then, you might have to just use Callable[..., TRet].