python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.5k stars 2.83k forks source link

TypeVar bound to Never when mypy can't infer the appropriate value #18154

Open fofoni opened 1 day ago

fofoni commented 1 day ago

In the second call to decorator_factory() with no arguments, _T should be bound to int. I understand that inferring _T=int is maybe too much for mypy, but I believe _T should then be bound to Any, or maybe object to be extra strict, but definitely not Never, which is what currently happens.

https://mypy-play.net/?mypy=master&python=3.13&gist=4a1aeecb04582945c4fa78f196bace5f

from collections.abc import Callable
from typing import Any, Generic, ParamSpec, TypeVar

_T = TypeVar("_T", covariant=True)
_P = ParamSpec("_P")

class Box(Generic[_T]): ...

def decorator_factory(box_type: type[Box[_T]] = Box) -> Callable[
    [Callable[_P, _T]],
    Callable[_P, _T]
]:
    # Implementation doesn't matter
    x: Any
    return x

class Class:
    @decorator_factory(box_type=Box[int])
    def method_one(self) -> int:
        return 0

    @decorator_factory()
    def method_two(self) -> int:
        return 0

For the record, pyright has no complaints about this code.

brianschubert commented 1 day ago

As a workaround, you can try giving _T a default type argument. For example:

_T = TypeVar("_T", covariant=True, default=int)

reveal_type(decorator_factory())   # N: "def [_P] (def (*_P.args, **_P.kwargs) -> builtins.int) -> def (*_P.args, **_P.kwargs) -> builtins.int"
reveal_type(Class.method_one)      # N: "def (self: SCRATCH.Class) -> builtins.int"
reveal_type(Class().method_one())  # N: "builtins.int"

Using default=Any or default=object would also work, depending on what makes the most sense in your application.

For the record, while pyright doesn't complain, it's just as confused as mypy about how to bind _T, just silently so :wink::

# pyright 1.1.389
reveal_type(decorator_factory())   # Type of "decorator_factory()" is "((**_P@decorator_factory) -> Unknown) -> ((**_P@decorator_factory) -> Unknown)"
reveal_type(Class.method_two)      # Type of "Class.method_two" is "(self: Class) -> Unknown"
reveal_type(Class().method_two())  # Type of "Class().method_two()" is "Unknown"
erictraut commented 1 day ago

while pyright doesn't complain, it's just as confused as mypy

Pyright is not confused here; it's working correctly. It binds the TypeVar based on the default value Box which is equivalent to Box[Unknown]. (Pyright uses Unknown to refer to an implied Any.)