4

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.

user26785
  • 302
  • 1
  • 8
  • 2
    Does this answer your question? [Type annotations for \*args and \*\*kwargs](https://stackoverflow.com/questions/37031928/type-annotations-for-args-and-kwargs) – 0stone0 May 19 '22 at 12:20
  • `def foo(*args: str, **kwds: int):` – 0stone0 May 19 '22 at 12:20
  • Not at all... I am looking for a general solution for half a dozen of variously typed arguments. I'll update the question. – user26785 May 19 '22 at 12:31
  • If you know you want `n: int` and `s: str`, then add them to the signature. No reason to make `b` so generic. (What's the purpose of `b` if it does nothing *except* call `a`?) – chepner May 19 '22 at 13:02
  • @chepner I think OP gave us a minimal example to illustrate the question, removing anything not relevant to the question. – joanis May 19 '22 at 13:07
  • I think it removed things that *were* relevant, because `b` as defined makes no sense (might as well just write `b = a`). – chepner May 19 '22 at 13:29
  • There are cases when we need this kind of wrapping, e.g. converting an instance method to a callable, or writing a decorator, or some cases of delegation. I did try for a minimalistic example. – user26785 May 19 '22 at 13:37
  • @chepner , "There is no way because using the language this way is not idiomatic" is an answer too. I am not sure I'll like it, but I will accept it. :) – user26785 May 19 '22 at 13:39
  • It is implemented for `*args` (PEP for 3.11, but backport in `typing_extensions` exists): [pep](https://peps.python.org/pep-0646/). For `**kwargs` - not yet, there was a huge discussion, there are plans to implement similar `Unpack` for `TypedDict` to support `**kwargs`, but it will take time, now there is no draft pep for this - we're waiting! – STerliakov May 19 '22 at 19:21
  • If you preserve the signature of inner function, you can use [this technique](https://mypy-play.net/?mypy=latest&python=3.10&gist=fcb4ed5ff1a0beba23169ba329ba75a0) (gist mine), plus [`typing.Concatenate`](https://docs.python.org/3/library/typing.html#typing.Concatenate) if you need to change the signature. But it will require separate decorator for every updated function. – STerliakov May 19 '22 at 21:15
  • @SUTerliakov , your comments look like they are the answer... Could you post them as such? – user26785 May 20 '22 at 13:30

1 Answers1

1

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"
STerliakov
  • 4,983
  • 3
  • 15
  • 37