Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inferring argument types from **args

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.

like image 717
user26785 Avatar asked Dec 11 '25 04:12

user26785


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"
like image 179
STerliakov Avatar answered Dec 14 '25 00:12

STerliakov