mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Type Aliases that are generic over ParamSpec don't work

Open A5rocks opened this issue 3 years ago • 6 comments

Bug Report

I expect n = Callable[P, int] to work, or at least n: TypeAlias = Callable[P, int]

To Reproduce

from typing import ParamSpec, Callable, TypeAlias

P = ParamSpec("P")

c: TypeAlias = Callable[P, int]

def f(n: c[P]) -> c[P]:
    ...

Expected Behavior

No errors

Actual Behavior

main.py:5: error: Variable "typing.TypeAlias" is not valid as a type
main.py:5: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
main.py:7: error: Variable "__main__.c" is not valid as a type
main.py:7: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
main.py:7: error: Invalid location for ParamSpec "P"
main.py:7: note: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]'

Your Environment

mypy playground

A5rocks avatar Dec 28 '21 05:12 A5rocks

This is a bit annoying to do so far, as we have to return a CallableType as a type alias but... like... we need to mark that it is missing a ParamSpec. This is OK with TypeVars because [<unbound T>] is perfectly fine (representation wise), but there's no way to represent that a ParamSpec is unbound because CallableType expects some arguments which is aaaaaaaa

I think the best way will be to make essentially a sentinel value and check for that, though it's been annoying to do so far.

A5rocks avatar Jan 03 '22 03:01 A5rocks

Another example with Concatenate inside a Type Alias. Saw it in the last pyright release. https://github.com/microsoft/pyright/commit/2f5046b54523e9b04707da25d00118700953ebd7

import asyncio
from typing import Coroutine, Any, TypeVar, Concatenate, Callable, ParamSpec

_T = TypeVar("_T")
_P = ParamSpec("_P")

Coro = Coroutine[Any, Any, _T]
CoroFunc = Callable[_P, Coro[_T]]


class ClassA: ...

CheckFunc = CoroFunc[Concatenate[ClassA, _P], bool]


async def my_check_func(obj: ClassA, a: int, b: str) -> bool:
    print(a, b)
    return str(a) == b


async def takes_check_func(
    check_func: CheckFunc[_P], *args: _P.args, **kwargs: _P.kwargs
):
    await check_func(ClassA(), *args, **kwargs)


asyncio.run(takes_check_func(my_check_func, 1, "2"))

# This should generate an error because the signature doesn't match.
asyncio.run(takes_check_func(my_check_func, 1, 2))

cdce8p avatar Mar 18 '22 00:03 cdce8p

This may also be related?

https://mypy-play.net/?mypy=latest&python=3.10&gist=ae5f16e9b9ca9346f8a28ab60bd9825f

from typing import ParamSpec, Callable, Optional, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

x: Callable[P, R]  # error
y: Optional[Callable[P, R]]  # error
z: Callable[..., R]  # fine
main.py:6: error: The first argument to Callable must be a list of types or "..."
main.py:7: error: The first argument to Callable must be a list of types or "..."
Found 2 errors in 1 file (checked 1 source file)

JMMarchant avatar May 18 '22 12:05 JMMarchant

Also this:

https://mypy-play.net/?mypy=latest&python=3.10&gist=c4d6f13911151a073aed0d6650e4af3e

from typing import ParamSpec, Callable, Optional, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def target_func(x: int) -> int:
    return 0

def test(x: Callable[P, R]):
    pass

def test_optional(x: Optional[Callable[P, R]] = None):
    pass

test(target_func)  # fine
test_optional(target_func)  # error
main.py:16: error: Argument 1 to "test_optional" has incompatible type "Callable[[int], int]"; expected "Optional[Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing>]]"
Found 1 error in 1 file (checked 1 source file)

JMMarchant avatar May 18 '22 12:05 JMMarchant

In my case I was trying to annotate a decorator, e.g.

P = ParamSpec("P")
R = TypeVar("R")
C = Callable[P, R]
D = Callable[[C], C]

@overload
def my_decorator(arg: logging.Logger, level: str= "debug") -> D:
    ...

@overload
def my_decorator(arg: C, /) -> C:
    ...

def my_decorator(arg: Union[C, logging.Logger], level: str= "debug") -> Union[C, D]:
    ...

instead of

P = ParamSpec("P")
R = TypeVar("R")

def my_decorator(arg: Union[Callable[P, R], logging.Logger], level: str= "debug"
    ) -> Union[Callable[[Callable[P, R]], Callable[P, R]], Callable[P, R]]:

which could be used as

@my_decorator
def my_function():
    pass

or

@my_decorator(logger, level="info")
def my_function():
    pass

Having an alias would make everything much simpler.

PS: Would be cool as well if an unbound Callable had implicit ParamSpec("P") and TypeVar("R") as return value by default, e.g., this could work:


C = Callable
def my_decorator(arg: Union[C, logging.Logger], level: str= "debug") -> Union[Callable[[C], C], C]:
    def wrapper0(fn0: Callable[C.params, C.returns]) -> C.returns:
        ....
    def wrapper1(*args: C.params.args, **kwargs: C.params.kwargs]) -> C.returns:
        ....

But that would require a PEP.

joaoe avatar Jun 20 '22 11:06 joaoe

Another case which also throws another funny error on Concatenate (?)

from typing import ParamSpec, Callable, Concatenate

class Event: ...

P = ParamSpec("P")

EventHandler = Callable[Concatenate[Event, P], None]
main.py:7: error: The first argument to Callable must be a list of types, parameter specification, or "..."
main.py:7: note: See https://mypy.readthedocs.io/en/stable/kinds_of_types.html#callable-types-and-lambdas
main.py:7: error: The last parameter to Concatenate needs to be a ParamSpec
main.py:7: error: ParamSpec "P" is unbound
Found 3 errors in 1 file (checked 1 source file)

mypy playground

Potentially related issues: #13403 and #13476

mttbernardini avatar Sep 06 '22 12:09 mttbernardini

I'm experiencing this issue too, is there a workaround for this?

GideonBear avatar Nov 02 '22 14:11 GideonBear

Possible MFE (minimum failing example):

from typing import Callable, ParamSpec

P = ParamSpec("P")
FunctionType = Callable[P, None]
main.py:4: error: The first argument to Callable must be a list of types, parameter specification, or "..."
main.py:4: note: See https://mypy.readthedocs.io/en/stable/kinds_of_types.html#callable-types-and-lambdas
Found 1 error in 1 file (checked 1 source file)

mypy playgroud

abid-mujtaba avatar Nov 05 '22 13:11 abid-mujtaba

The issue I reported in https://github.com/python/mypy/issues/14069 is likely a symptom of this issue (and lack of alias support)

gandhis1 avatar Nov 17 '22 03:11 gandhis1

Is there any information on when this fix would be available? (When 0.992 is released?)

RRRajput avatar Dec 08 '22 10:12 RRRajput

Is there any information on when this fix would be available? (When 0.992 is released?)

You can track progress on the next release by following #13685 (I think it's unlikely that there'll be a 0.992 release).

AlexWaygood avatar Dec 08 '22 10:12 AlexWaygood

The following is not working for me in 1.0.0:

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 
  • https://mypy-play.net/?mypy=latest&python=3.11&gist=f4c26907bfc0ae0118b90c1fa5a79fe8
  • https://stackoverflow.com/questions/75390522

randolf-scholz avatar Feb 08 '23 19:02 randolf-scholz

@randolf-scholz, you have defined a generic type alias that has a single type parameter (P), but when you use the type alias in the last line, you're not specifying a type argument. Mypy should assume ... in this case (just like it assumes Any for a regular type variable). If you explicitly supply the type argument (a: intfun[...] or a: intfun[[]]), mypy succeeds.

So I agree this is a bug in mypy, but your code is also probably not doing what you intended. You might want to file a separate bug since this one has been closed for a while.

erictraut avatar Feb 08 '23 19:02 erictraut

@erictraut So when I use intfun[...], then mypy reveals the type as def (builtins.int, *Any, **Any). Is there any way to directly specify this exact type hint as a TypeAlias? It seems weird that one has to go this roundabout way, as neither Callable[[int, ...], None], nor Callable[Concatenate[int, ...], None] are supported.

randolf-scholz avatar Feb 08 '23 20:02 randolf-scholz

I recommend using a callback protocol

hauntsaninja avatar Feb 08 '23 21:02 hauntsaninja

@hauntsaninja Not sure how that would look like, when one writes a Callback-Protocol, how do you make the signature generic? I created a mypy-play with a bunch of test-cases here:

  • https://gist.github.com/2ba6f03063086aec23f6a6cad11d28fc
  • https://mypy-play.net/?mypy=latest&python=3.11&gist=2ba6f03063086aec23f6a6cad11d28fc
  • https://github.com/python/typing/discussions/1346

EDIT: Found some older threads about this

  • https://github.com/python/mypy/issues/8263
  • https://github.com/python/typing/issues/696
  • https://github.com/python/cpython/pull/30969
  • https://bugs.python.org/issue44791

randolf-scholz avatar Feb 08 '23 21:02 randolf-scholz

Oh thanks, I see. Basically the Concatenate version of https://github.com/python/mypy/issues/5876

hauntsaninja avatar Feb 08 '23 21:02 hauntsaninja