mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Incorrect resolution of `type[TypeVar]` returning `TypeVar` with `numpy`

Open Dr-Irv opened this issue 1 month ago • 7 comments

Bug Report

This came up with pandas-stubs, but can reproduce with just numpy

To Reproduce

from typing import TypeVar
import numpy as np

NDArrayT = TypeVar("NDArrayT", bound=np.ndarray)


def foo(x: type[NDArrayT]) -> NDArrayT:
    return x([1, 2])


reveal_type(foo(np.ndarray))

Expected Behavior

Revealed type should be ndarray[tuple[Any, ...], dtype[Any]], which is what pyright reports.

Actual Behavior

mypy reports numpy.ndarray[Any, Any]

Your Environment

  • Mypy version used: 1.19.0
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.11
  • numpy version: 2.3.5
  • pyright version: 1.1.407

Dr-Irv avatar Dec 02 '25 15:12 Dr-Irv

I thought this might be related to the PEP 696 type parameter defaults, but that doesn't seem to be the case:

from typing import Any, reveal_type

import numpy as np

def f[T: np.ndarray](t: type[T]) -> T: ...
def g[T: np.ndarray[tuple[Any, ...], np.dtype[Any]]](t: type[T]) -> T: ...

reveal_type(f(np.ndarray))  # Revealed type is "numpy.ndarray[Any, Any]"
reveal_type(g(np.ndarray))  # Revealed type is "numpy.ndarray[Any, Any]"

In fact, any type arguments to np.ndarray seem to be swallowed this way 🤔

jorenham avatar Dec 02 '25 16:12 jorenham

Could you explain further why you expect different output? Any is compatible with any bound, type passed as argument is not parameterized hence defaults all tvars to any, why should it not be Any?

Zero-numpy example (playground):

from typing import Any, Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U", bound="Arr[int]")

class Arr(Generic[T]):
    ...

def f(t: type[U]) -> U: ...

reveal_type(f(Arr))  # Revealed type is "__main__.Arr[Any]"

sterliakov avatar Dec 02 '25 22:12 sterliakov

type passed as argument is not parameterized hence defaults all tvars to any,

As my example shows, this also occurs when its fully parametrized. Also note that the type parameter defaults of np.ndarray are tuple[Any, ...] and np.dtype[Any]. So np.ndarray (as type expression) is therefore exactly equivalent to np.ndarray[tuple[Any, ...], np.dtype[Any]].

jorenham avatar Dec 02 '25 22:12 jorenham

Ah, sorry, I missed that numpy already started using tvars defaults. So the problem reduces to the following, right?

from typing import Any, Generic, TypeVar

T = TypeVar("T", default=int)
U = TypeVar("U", bound="Arr")

class Arr(Generic[T]):
    ...

def f(t: type[U]) -> U: ...

reveal_type(f(Arr))  # Revealed type is "__main__.Arr[Any]"

Probably the default should have been applied here.

sterliakov avatar Dec 02 '25 22:12 sterliakov

So the problem reduces to the following, right?

Yea pretty much. There's also where f is parametrized with Arr[int] (so with type arg) that behaves unexpectedly:

# mypy: disable-error-code=empty-body
from typing import Any, Generic, TypeVar

T_co = TypeVar("T_co", default=int)
class Arr(Generic[T_co]): ...

def f[T: Arr](t: type[T]) -> T: ...
def g[T: Arr[int]](t: type[T]) -> T: ...

reveal_type(f(Arr))  # Revealed type is "__main__.Arr[Any]"
reveal_type(g(Arr))  # Revealed type is "__main__.Arr[Any]"

https://mypy-play.net/?mypy=latest&python=3.14&flags=strict&gist=b51972fc19e7c56363428c62dede27e7

jorenham avatar Dec 02 '25 23:12 jorenham

Just a hunch but wouldn't this be one of the cases covered by PEP 747?

jacopoabramo avatar Dec 05 '25 15:12 jacopoabramo

Nah, this is the correct use for type - TypeForm is not needed when you have an actual class. TypeForm is compatible with things like TypeVar itself, should be irrelevant here - the problem is not evaluating Arr in f(Arr) as if it was in a type context (failure to resolve the typevar to its default). The snippet is correct, rejecting it is a defect in mypy.

sterliakov avatar Dec 05 '25 15:12 sterliakov