2

Similar to Python type hint Callable with one known positional type and then *args and **kwargs, I want to type hint a Callable for which is known:

  1. It must have at least 1 positional input.
  2. The first positional input must be int.
  3. It must return None.

Apart from that, any signature is valid. I tried to do the following, but it doesn't work. So, is it possible to do it in python 3.10/3.11 at all?

from typing import TypeAlias, ParamSpec, Concatenate, Callable

P = ParamSpec("P")
intfun: TypeAlias = Callable[Concatenate[int, P], None]

def foo(i: int) -> None:
    pass

a: intfun = foo  # ✘ Incompatible types in assignment 
# expression has type "Callable[[int], None]", 
# variable has type "Callable[[int, VarArg(Any), KwArg(Any)], None]")

https://mypy-play.net/?mypy=latest&python=3.11&gist=f4c26907bfc0ae0118b90c1fa5a79fe8

I am using mypy==1.0.0.

Context: I want to type hint a dict hat holds key-value pairs where the value could be any Callable satisfying properties 1,2,3.

STerliakov
  • 4,983
  • 3
  • 15
  • 37
Hyperplane
  • 1,422
  • 1
  • 14
  • 28
  • 1
    Related: https://stackoverflow.com/questions/74893354/is-literal-ellipsis-really-valid-as-paramspec-last-argument – STerliakov Feb 08 '23 at 21:34

1 Answers1

0

The somewhat cryptic notation used by mypy for the type you annotated a with indicates that it does not implicitly specify the generic Callable you defined to intfun[...] (literal ellipses here), when you omit type arguments. As pointed out here, there is a case to be made for mypy to simply assume that like it assumes an Any argument for "normal" generics.

But that is beside the point here because when you are dealing with generics you should treat them as such. Essentially the P is the type variable here, so whenever you want to use intfun for annotation, you should either specify it or bind another type variable, depending on the context.

In this case, you can just do a: intfun[[]] = foo and the code will pass the type check. That annotation specifies intfun to callables that have no other parameters (besides the mandatory int). Thus the following will cause an error in the last line:

from typing import TypeAlias, ParamSpec, Concatenate, Callable

P = ParamSpec("P")
intfun: TypeAlias = Callable[Concatenate[int, P], None]


def foo(i: int) -> None:
    pass


def bar(i: int, s: str) -> None:
    pass


a: intfun[[]] = foo  # passes
a = bar  # error

Depending on the context, in which you want to use that type alias, you may want to provide a different type argument. Here is another example, where you use intfun in its generic form:

from typing import TypeAlias, ParamSpec, Concatenate, Callable

P = ParamSpec("P")
intfun: TypeAlias = Callable[Concatenate[int, P], None]


def foo(i: int) -> None:
    pass


def baz(f: intfun[P]) -> Callable[Concatenate[int, P], int]:
    def wrapper(i: int, /, *args: P.args, **kwargs: P.kwargs) -> int:
        f(i, *args, **kwargs)
        return i
    return wrapper


fooz = baz(foo)  # passes
Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
  • Unfortunately, that doesn't work for me. I added some context to the question: I want to type hint a `dict` whose values are `Callable`'s that satisfy properties 1, 2 and 3. They could or couldn't have any number of mandatory/optional extra arguments. It's just that I somehow want to formulate the critical condition that the first positional argument must be `int`. – Hyperplane Feb 08 '23 at 20:40
  • I suppose that possibly it's not currently solvable, as `ParamSpec` is not strong enough to type annotate `functools.partial` either, and essentially this is about type hinting partially known signatures. – Hyperplane Feb 08 '23 at 20:44
  • 1
    @Hyperplane and this raises an important question: why do you need to annotate that property? It's absolutely useless for any external caller, because he still doesn't know the signature (e.g. will `your_dict[some_key](1, 'foo')` work? First argument is int, so it should be OK? But we know nothing about the rest, so it may raise). And if you can't use this information on read, why check it on write? I can't see why this annotation can help anybody (so that `Callable[..., None]` is not sufficient), please clarify this point. – STerliakov Feb 08 '23 at 21:50
  • @SUTerliakov The missing arguments will either be optional, or provided via a config and usage of `functools.partial`. – Hyperplane Feb 08 '23 at 21:51