pyright icon indicating copy to clipboard operation
pyright copied to clipboard

Referring to a different instantiation whilst defining a generic class

Open mniip opened this issue 4 months ago • 4 comments

The following generic generic class and a function to construct it type-checks fine:

from __future__ import annotations
from typing import TypeVar, Generic
T = TypeVar("T")
S = TypeVar("S")

class C(Generic[T]):
    pass

def standalone(_dummy: S | None = None) -> C[S]:
    ty: type[C[S]] = C
    return object.__new__(ty)

But trying to move standalone into C as any kind of method breaks it:

from __future__ import annotations
from typing import TypeVar, Generic
T = TypeVar("T")
S = TypeVar("S")

class C(Generic[T]):
    def meth(self, _dummy: S | None = None) -> C[S]:
        ty: type[C[S]] = C
        return object.__new__(ty)

    @classmethod
    def classmeth(cls, _dummy: S | None = None) -> C[S]:
        ty: type[C[S]] = C
        return object.__new__(ty)

    @staticmethod
    def staticmeth(_dummy: S | None = None) -> C[S]:
        ty: type[C[S]] = C
        return object.__new__(ty)
test.py
  test.py:8:26 - error: Type "type[C[T@C]]" is not assignable to declared type "type[C[S@meth]]"
    "type[C[T@C]]" is not assignable to "type[C[S@meth]]"
    Type "type[C[T@C]]" is not assignable to type "type[C[S@meth]]"
      Type parameter "T@C" is invariant, but "T@C" is not the same as "S@meth" (reportAssignmentType)
  test.py:13:26 - error: Type "type[C[T@C]]" is not assignable to declared type "type[C[S@classmeth]]"
    "type[C[T@C]]" is not assignable to "type[C[S@classmeth]]"
    Type "type[C[T@C]]" is not assignable to type "type[C[S@classmeth]]"
      Type parameter "T@C" is invariant, but "T@C" is not the same as "S@classmeth" (reportAssignmentType)
  test.py:18:26 - error: Type "type[C[T@C]]" is not assignable to declared type "type[C[S@staticmeth]]"
    "type[C[T@C]]" is not assignable to "type[C[S@staticmeth]]"
    Type "type[C[T@C]]" is not assignable to type "type[C[S@staticmeth]]"
      Type parameter "T@C" is invariant, but "T@C" is not the same as "S@staticmeth" (reportAssignmentType)
3 errors, 0 warnings, 0 informations 

Notably, reveal_type(C) shows type[C[Unknown]] in all four contexts.

Additional context I would like to return object.__new__(C) but then Pyright cannot infer the fact that C is used "at type S", and instead infers that we've passed a type[C[Unknown]] into object.__new__ and have gotten a C[Unknown] back, which doesn't match with the expected return type C[S].

I can work around this by first placing C into a type-annotated variable, thus

ty: type[C[S]] = C
return object.__new__(ty)

but for some reason this only works in a standalone function context (hence this bug report).

Notably, if I do return object.__new__(C[S]) this satisfies pyright, but instead makes cpython unhappy: at runtime we get TypeError: object.__new__(X): X is not a type object (_GenericAlias). Same error for return C[S].__new__(C[S])... which is a little odd, because isn't this supposed to be equivalent to C[S]()?

VS Code extension or command-line Pyright 1.1.403

mniip avatar Aug 10 '25 00:08 mniip

Hmm, I agree this looks like a bug, but I'm curious why you're using this approach rather than simply specifying C[S]? By doing this, you indicate that you want an explicit specialization of C.

    def meth(self, _dummy: S | None = None) -> C[S]:
        ty = C[S]
        return object.__new__(ty)

erictraut avatar Aug 10 '25 01:08 erictraut

ty = C[S]
return object.__new__(ty)

As I explained in additional context, this typechecks but doesn't actually run:

>>> object.__new__(C[S])
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    object.__new__(C[S])
    ~~~~~~~~~~~~~~^^^^^^
TypeError: object.__new__(X): X is not a type object (_GenericAlias)

mniip avatar Aug 10 '25 09:08 mniip

Ah, I missed that in your original post. I'm surprised that C[S] doesn't work here. This might be a bug in the CPython implementation. @JelleZijlstra, any thoughts on this?

erictraut avatar Aug 10 '25 16:08 erictraut

The object.__new__ approach requires an actual type (an instance of builtins.type, not a conceptual type in the type system). I think that's working correctly.

Why are you using object.__new__ in this way? It seems a bit odd and I'm not sure we should expect type checkers to support it.

JelleZijlstra avatar Aug 11 '25 04:08 JelleZijlstra