Inferring argument types from **args

Viewed 95

Let's say I have code like this:

def a(n:int = 10, s:str = "")->int:
    return n 

def b(**args):
    return a(**args)

Is there any way to tell python that b takes one argument named n that is an integer and another s that is a string? In other words, is there a way to pass the typing through **dict?

Update: to be clear, I am looking for a general solution that would work for arbitrary number of arguments with arbitrary types. My goal is to follow the DRY principle when it comes to specifying argument types.

1 Answers

This is an old story, which was discussed for a long time. Here's mypy issue related to this problem (still open since 2018), it is even mentioned in PEP-589. Some steps were taken in right direction: python 3.11 introduced Unpack and allowed star unpacking in annotations - it was designed together with variadic generics support, see PEP-646 (backported to typing_extensions, but no mypy support yet AFAIC). But it works only for *args, **kwargs construction is still waiting.

However, it is possible with additional efforts. You can create your own decorator that can convince mypy that function has expected signature (playground):

from typing import Any, Callable, TypeVar, cast

_C = TypeVar('_C', bound=Callable)

def preserve_sig(func: _C) -> Callable[[Callable], _C]:
    def wrapper(f: Callable) -> _C:
        return cast(_C, f)
    return wrapper

def f(x: int, y: str = 'foo') -> int:
    return 1

@preserve_sig(f)
def g(**kwargs: Any) -> int:
    return f(**kwargs)

g(x=1, y='bar')
g(z=0)  # E: Unexpected keyword argument "z" for "g"

You can even alter function signature, appending or prepending new arguments, with PEP-612 (playground:

from functools import wraps
from typing import Any, Callable, Concatenate, ParamSpec, TypeVar, cast

_R = TypeVar('_R')
_P = ParamSpec('_P')


def alter_sig(func: Callable[_P, _R]) -> Callable[[Callable], Callable[Concatenate[int, _P], _R]]:
    
    def wrapper(f: Callable) -> Callable[Concatenate[int, _P], _R]:
        
        @wraps(f)
        def inner(num: int, *args: _P.args, **kwargs: _P.kwargs):
            print(num)
            return f(*args, **kwargs)
            
        return inner
    
    return wrapper

def f(x: int, y: str = 'foo') -> int:
    return 1

@alter_sig(f)
def g(**kwargs: Any) -> int:
    return f(**kwargs)

g(1, x=1, y='bar')
g(1, 2, 'bar')
g(1, 2)
g(x=1, y='bar')  # E: Too few arguments for "g"
g(1, 'baz')  # E: Argument 2 to "g" has incompatible type "str"; expected "int"
g(z=0)  # E: Unexpected keyword argument "z" for "g"
Related