pythonmypypython-typingkeyword-argument

How to hint type a Callable without knowing all the necessary args and kwargs?


I would like to code a wrapper that takes in arguments a function, its args and kwargs and execute them but with some kwargs precisely known while others are unknown.

Exemple:

def wrapper(custom_function: MyType, a: int, b: str, *args, **kwargs) -> float:
    print(a+3)
    print(b)
    return custom_function(a, b, *args, **kwargs)

In this exemple I want to execute any function with first argument named a with type int and second named b with type str but I don't care about any other argument.

With this wrapper, I would like the following type hints to succeed or fail:

def f1(a: int, b: str) -> float:
    ...

def f2(a: int, b: str, c: float) -> float:
    ...

def f3(a: str, b: str) -> float:
    ...

def f4(a: int, b: str, *args, **kwargs) -> float:
    ...


wrapper(f1, 1, "a")            # test 1: succeed
wrapper(f2, 1, "a", 4.6)       # test 2: succeed
wrapper(f3, 1, "a")            # test 3: fail
wrapper(f4, 1, "a", [1, 2, 3]) # test 4: succeed

I tried using typing.Protocol and typing.ParamSpec in the following way:

P = ParamSpec("P")

class MyType(Protocol):
    def __call__(self, a: int, b: str, P):
        ....

but it does not work (test 1 and 2 fail)

I guess using Callable[...] like this would be the nearest I can get:

MyType = Callable[..., float]

but this solution does not satisfy me since the test 3 would succeed while I would like it to fail.

Is what I am looking for impossible ?


Solution

  • I would like to code a wrapper that takes in arguments a function, its args and kwargs and execute them but with some kwargs precisely known while others are unknown.

    This is exactly the job for typing.Concatenate. Your example is a modified version of the typing docs example, but instead of returning a wrapped custom_function with a modified signature, your example just invokes custom_function directly.

    from typing import Callable, Concatenate, ParamSpec, TypeVar
    
    P = ParamSpec("P")
    R = TypeVar("R")
    
    def wrapper(custom_function: Callable[Concatenate[int, str, P], R], a: int, b: str, *args: P.args, **kwargs: P.kwargs) -> R:
        print(a+3)
        print(b)
        return custom_function(a, b, *args, **kwargs)
    
    def f1(a: int, b: str) -> float:
        ...
    
    def f2(a: int, b: str, c: float) -> float:
        ...
    
    def f3(a: str, b: str) -> float:
        ...
    
    def f4(a: int, b: str, *args, **kwargs) -> float:
        ...
    
    wrapper(f1, 1, "a")            # OK
    wrapper(f2, 1, "a", 4.6)       # OK
    wrapper(f3, 1, "a")            # "int" is incompatible with "str"
    wrapper(f4, 1, "a", [1, 2, 3]) # OK