ruff/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
Carl Meyer 8248193ed9
[ty] defer inference of legacy TypeVar bound/constraints/defaults (#20598)
## Summary

This allows us to handle self-referential bounds/constraints/defaults
without panicking.

Handles more cases from https://github.com/astral-sh/ty/issues/256

This also changes the way we infer the types of legacy TypeVars. Rather
than understanding a constructor call to `typing[_extension].TypeVar`
inside of any (arbitrarily nested) expression, and having to use a
special `assigned_to` field of the semantic index to try to best-effort
figure out what name the typevar was assigned to, we instead understand
the creation of a legacy `TypeVar` only in the supported syntactic
position (RHS of a simple un-annotated assignment with one target). In
any other position, we just infer it as creating an opaque instance of
`typing.TypeVar`. (This behavior matches all other type checkers.)

So we now special-case TypeVar creation in `TypeInferenceBuilder`, as a
special case of an assignment definition, rather than deeper inside call
binding. This does mean we re-implement slightly more of
argument-parsing, but in practice this is minimal and easy to handle
correctly.

This is easier to implement if we also make the RHS of a simple (no
unpacking) one-target assignment statement no longer a standalone
expression. Which is fine to do, because simple one-target assignments
don't need to infer the RHS more than once. This is a bonus performance
(0-3% across various projects) and significant memory-usage win, since
most assignment statements are simple one-target assignment statements,
meaning we now create many fewer standalone-expression salsa
ingredients.

This change does mean that inference of manually-constructed
`TypeAliasType` instances can no longer find its Definition in
`assigned_to`, which regresses go-to-definition for these aliases. In a
future PR, `TypeAliasType` will receive the same treatment that
`TypeVar` did in this PR (moving its special-case inference into
`TypeInferenceBuilder` and supporting it only in the correct syntactic
position, and lazily inferring its value type to support recursion),
which will also fix the go-to-definition regression. (I decided a
temporary edge-case regression is better in this case than doubling the
size of this PR.)

This PR also tightens up and fixes various aspects of the validation of
`TypeVar` creation, as seen in the tests.

We still (for now) treat all typevars as instances of `typing.TypeVar`,
even if they were created using `typing_extensions.TypeVar`. This means
we'll wrongly error on e.g. `T.__default__` on Python 3.11, even if `T`
is a `typing_extensions.TypeVar` instance at runtime. We share this
wrong behavior with both mypy and pyrefly. It will be easier to fix
after we pull in https://github.com/python/typeshed/pull/14840.

There are some issues that showed up here with typevar identity and
`MarkTypeVarsInferable`; the fix here (using the new `original` field
and `is_identical_to` methods on `BoundTypeVarInstance` and
`TypeVarInstance`) is a bit kludgy, but it can go away when we eliminate
`MarkTypeVarsInferable`.

## Test Plan

Added and updated mdtests.

### Conformance suite impact

The impact here is all positive:

* We now correctly error on a legacy TypeVar with exactly one constraint
type given.
* We now correctly error on a legacy TypeVar with both an upper bound
and constraints specified.

### Ecosystem impact

Basically none; in the setuptools case we just issue slightly different
errors on an invalid TypeVar definition, due to the modified validation
code.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-10-09 21:08:37 +00:00

20 KiB

Generic classes: Legacy syntax

Defining a generic class

At its simplest, to define a generic class using the legacy syntax, you inherit from the typing.Generic special form, which is "specialized" with the generic class's type variables.

from ty_extensions import generic_context
from typing_extensions import Generic, TypeVar, TypeVarTuple, ParamSpec, Unpack

T = TypeVar("T")
S = TypeVar("S")
P = ParamSpec("P")
Ts = TypeVarTuple("Ts")

class SingleTypevar(Generic[T]): ...
class MultipleTypevars(Generic[T, S]): ...
class SingleParamSpec(Generic[P]): ...
class TypeVarAndParamSpec(Generic[P, T]): ...
class SingleTypeVarTuple(Generic[Unpack[Ts]]): ...
class TypeVarAndTypeVarTuple(Generic[T, Unpack[Ts]]): ...

# revealed: tuple[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))

# TODO: support `ParamSpec`/`TypeVarTuple` properly (these should not reveal `None`)
reveal_type(generic_context(SingleParamSpec))  # revealed: None
reveal_type(generic_context(TypeVarAndParamSpec))  # revealed: None
reveal_type(generic_context(SingleTypeVarTuple))  # revealed: None
reveal_type(generic_context(TypeVarAndTypeVarTuple))  # revealed: None

Inheriting from Generic multiple times yields a duplicate-base diagnostic, just like any other class:

class Bad(Generic[T], Generic[T]): ...  # error: [duplicate-base]
class AlsoBad(Generic[T], Generic[S]): ...  # error: [duplicate-base]

You cannot use the same typevar more than once.

# TODO: error
class RepeatedTypevar(Generic[T, T]): ...

You can only specialize typing.Generic with typevars (TODO: or param specs or typevar tuples).

# error: [invalid-argument-type] "`<class 'int'>` is not a valid argument to `Generic`"
class GenericOfType(Generic[int]): ...

You can also define a generic class by inheriting from some other generic class, and specializing it with typevars.

class InheritedGeneric(MultipleTypevars[T, S]): ...
class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ...
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...

# revealed: tuple[T@InheritedGeneric, S@InheritedGeneric]
reveal_type(generic_context(InheritedGeneric))
# revealed: tuple[T@InheritedGenericPartiallySpecialized]
reveal_type(generic_context(InheritedGenericPartiallySpecialized))
# revealed: None
reveal_type(generic_context(InheritedGenericFullySpecialized))

If you don't specialize a generic base class, we use the default specialization, which maps each typevar to its default value or Any. Since that base class is fully specialized, it does not make the inheriting class generic.

class InheritedGenericDefaultSpecialization(MultipleTypevars): ...

reveal_type(generic_context(InheritedGenericDefaultSpecialization))  # revealed: None

When inheriting from a generic class, you can optionally inherit from typing.Generic as well. But if you do, you have to mention all of the typevars that you use in your other base classes.

class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ...

# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ...
class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ...
class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ...

# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ...

# revealed: tuple[T@ExplicitInheritedGeneric, S@ExplicitInheritedGeneric]
reveal_type(generic_context(ExplicitInheritedGeneric))
# revealed: tuple[T@ExplicitInheritedGenericPartiallySpecialized]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized))
# revealed: tuple[T@ExplicitInheritedGenericPartiallySpecializedExtraTypevar, S@ExplicitInheritedGenericPartiallySpecializedExtraTypevar]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar))

Specializing generic classes explicitly

The type parameter can be specified explicitly:

from typing_extensions import Generic, Literal, TypeVar

T = TypeVar("T")

class C(Generic[T]):
    x: T

reveal_type(C[int]())  # revealed: C[int]
reveal_type(C[Literal[5]]())  # revealed: C[Literal[5]]

The specialization must match the generic types:

# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]())  # revealed: Unknown

If the type variable has an upper bound, the specialized type must satisfy that bound:

from typing import Union

BoundedT = TypeVar("BoundedT", bound=int)
BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str])

class Bounded(Generic[BoundedT]): ...
class BoundedByUnion(Generic[BoundedByUnionT]): ...
class IntSubclass(int): ...

reveal_type(Bounded[int]())  # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]())  # revealed: Bounded[IntSubclass]

# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]())  # revealed: Unknown

# TODO: update this diagnostic to talk about type parameters and specializations
# error:  [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]())  # revealed: Unknown

reveal_type(BoundedByUnion[int]())  # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]())  # revealed: BoundedByUnion[IntSubclass]
reveal_type(BoundedByUnion[str]())  # revealed: BoundedByUnion[str]
reveal_type(BoundedByUnion[int | str]())  # revealed: BoundedByUnion[int | str]

If the type variable is constrained, the specialized type must satisfy those constraints:

ConstrainedT = TypeVar("ConstrainedT", int, str)

class Constrained(Generic[ConstrainedT]): ...

reveal_type(Constrained[int]())  # revealed: Constrained[int]

# TODO: error: [invalid-argument-type]
# TODO: revealed: Constrained[Unknown]
reveal_type(Constrained[IntSubclass]())  # revealed: Constrained[IntSubclass]

reveal_type(Constrained[str]())  # revealed: Constrained[str]

# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]())  # revealed: Constrained[int | str]

# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to class `Constrained` is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]())  # revealed: Unknown

If the type variable has a default, it can be omitted:

WithDefaultU = TypeVar("WithDefaultU", default=int)

class WithDefault(Generic[T, WithDefaultU]): ...

reveal_type(WithDefault[str, str]())  # revealed: WithDefault[str, str]
reveal_type(WithDefault[str]())  # revealed: WithDefault[str, int]

Inferring generic class parameters

We can infer the type parameter from a type context:

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class C(Generic[T]):
    x: T

c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c)  # revealed: C[Unknown]

The typevars of a fully specialized generic class should no longer be visible:

# TODO: revealed: int
reveal_type(c.x)  # revealed: Unknown

If the type parameter is not specified explicitly, and there are no constraints that let us infer a specific type, we infer the typevar's default type:

DefaultT = TypeVar("DefaultT", default=int)

class D(Generic[DefaultT]): ...

reveal_type(D())  # revealed: D[int]

If a typevar does not provide a default, we use Unknown:

reveal_type(C())  # revealed: C[Unknown]

Inferring generic class parameters from constructors

If the type of a constructor parameter is a class typevar, we can use that to infer the type parameter. The types inferred from a type context and from a constructor parameter must be consistent with each other.

__new__ only

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class C(Generic[T]):
    def __new__(cls, x: T) -> "C[T]":
        return object.__new__(cls)

reveal_type(C(1))  # revealed: C[int]

# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")

__init__ only

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class C(Generic[T]):
    def __init__(self, x: T) -> None: ...

reveal_type(C(1))  # revealed: C[int]

# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")

Identical __new__ and __init__ signatures

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class C(Generic[T]):
    def __new__(cls, x: T) -> "C[T]":
        return object.__new__(cls)

    def __init__(self, x: T) -> None: ...

reveal_type(C(1))  # revealed: C[int]

# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")

Compatible __new__ and __init__ signatures

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class C(Generic[T]):
    def __new__(cls, *args, **kwargs) -> "C[T]":
        return object.__new__(cls)

    def __init__(self, x: T) -> None: ...

reveal_type(C(1))  # revealed: C[int]

# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")

class D(Generic[T]):
    def __new__(cls, x: T) -> "D[T]":
        return object.__new__(cls)

    def __init__(self, *args, **kwargs) -> None: ...

reveal_type(D(1))  # revealed: D[int]

# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")

Both present, __new__ inherited from a generic base class

If either method comes from a generic base class, we don't currently use its inferred specialization to specialize the class.

from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")

class C(Generic[T, U]):
    def __new__(cls, *args, **kwargs) -> "C[T, U]":
        return object.__new__(cls)

class D(C[V, int]):
    def __init__(self, x: V) -> None: ...

reveal_type(D(1))  # revealed: D[int]

Generic class inherits __init__ from generic base class

from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class C(Generic[T, U]):
    def __init__(self, t: T, u: U) -> None: ...

class D(C[T, U]):
    pass

reveal_type(C(1, "str"))  # revealed: C[int, str]
reveal_type(D(1, "str"))  # revealed: D[int, str]

Generic class inherits __init__ from dict

This is a specific example of the above, since it was reported specifically by a user.

from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class D(dict[T, U]):
    pass

reveal_type(D(key=1))  # revealed: D[str, int]

Generic class inherits __new__ from tuple

(Technically, we synthesize a __new__ method that is more precise than the one defined in typeshed for tuple, so we use a different mechanism to make sure it has the right inherited generic context. But from the user's point of view, this is another example of the above.)

from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class C(tuple[T, U]): ...

reveal_type(C((1, 2)))  # revealed: C[int, int]

Upcasting a tuple to its Sequence supertype

This test is taken from the typing spec conformance suite

[environment]
python-version = "3.11"
from typing_extensions import TypeVar, Sequence, Never

T = TypeVar("T")

def test_seq(x: Sequence[T]) -> Sequence[T]:
    return x

def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: tuple[()]):
    reveal_type(test_seq(t1))  # revealed: Sequence[int | float | complex | list[int]]
    reveal_type(test_seq(t2))  # revealed: Sequence[int | str]

    # TODO: this should be `Sequence[Never]`
    reveal_type(test_seq(t3))  # revealed: Sequence[Unknown]

__init__ is itself generic

from typing_extensions import Generic, TypeVar

S = TypeVar("S")
T = TypeVar("T")

class C(Generic[T]):
    def __init__(self, x: T, y: S) -> None: ...

reveal_type(C(1, 1))  # revealed: C[int]
reveal_type(C(1, "string"))  # revealed: C[int]
reveal_type(C(1, True))  # revealed: C[int]

# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)

Some __init__ overloads only apply to certain specializations

from typing_extensions import overload, Generic, TypeVar

T = TypeVar("T")

class C(Generic[T]):
    @overload
    def __init__(self: "C[str]", x: str) -> None: ...
    @overload
    def __init__(self: "C[bytes]", x: bytes) -> None: ...
    @overload
    def __init__(self: "C[int]", x: bytes) -> None: ...
    @overload
    def __init__(self, x: int) -> None: ...
    def __init__(self, x: str | bytes | int) -> None: ...

reveal_type(C("string"))  # revealed: C[str]
reveal_type(C(b"bytes"))  # revealed: C[bytes]
reveal_type(C(12))  # revealed: C[Unknown]

C[str]("string")
C[str](b"bytes")  # error: [no-matching-overload]
C[str](12)

C[bytes]("string")  # error: [no-matching-overload]
C[bytes](b"bytes")
C[bytes](12)

C[int]("string")  # error: [no-matching-overload]
C[int](b"bytes")
C[int](12)

C[None]("string")  # error: [no-matching-overload]
C[None](b"bytes")  # error: [no-matching-overload]
C[None](12)

Synthesized methods with dataclasses

from dataclasses import dataclass
from typing_extensions import Generic, TypeVar

T = TypeVar("T")

@dataclass
class A(Generic[T]):
    x: T

reveal_type(A(x=1))  # revealed: A[int]

Class typevar has another typevar as a default

from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U", default=T)

class C(Generic[T, U]): ...

reveal_type(C())  # revealed: C[Unknown, Unknown]

class D(Generic[T, U]):
    def __init__(self) -> None: ...

reveal_type(D())  # revealed: D[Unknown, Unknown]

Generic subclass

When a generic subclass fills its superclass's type parameter with one of its own, the actual types propagate through:

from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")
W = TypeVar("W")

class Parent(Generic[T]):
    x: T

class ExplicitlyGenericChild(Parent[U], Generic[U]): ...
class ExplicitlyGenericGrandchild(ExplicitlyGenericChild[V], Generic[V]): ...
class ExplicitlyGenericGreatgrandchild(ExplicitlyGenericGrandchild[W], Generic[W]): ...
class ImplicitlyGenericChild(Parent[U]): ...
class ImplicitlyGenericGrandchild(ImplicitlyGenericChild[V]): ...
class ImplicitlyGenericGreatgrandchild(ImplicitlyGenericGrandchild[W]): ...

reveal_type(Parent[int]().x)  # revealed: int
reveal_type(ExplicitlyGenericChild[int]().x)  # revealed: int
reveal_type(ImplicitlyGenericChild[int]().x)  # revealed: int
reveal_type(ExplicitlyGenericGrandchild[int]().x)  # revealed: int
reveal_type(ImplicitlyGenericGrandchild[int]().x)  # revealed: int
reveal_type(ExplicitlyGenericGreatgrandchild[int]().x)  # revealed: int
reveal_type(ImplicitlyGenericGreatgrandchild[int]().x)  # revealed: int

Generic methods

Generic classes can contain methods that are themselves generic. The generic methods can refer to the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in scope for the method.

from ty_extensions import generic_context
from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class C(Generic[T]):
    def method(self, u: int) -> int:
        return u

    def generic_method(self, t: T, u: U) -> U:
        return u

reveal_type(generic_context(C))  # revealed: tuple[T@C]
reveal_type(generic_context(C.method))  # revealed: tuple[Self@method]
reveal_type(generic_context(C.generic_method))  # revealed: tuple[Self@generic_method, U@generic_method]
reveal_type(generic_context(C[int]))  # revealed: None
reveal_type(generic_context(C[int].method))  # revealed: tuple[Self@method]
reveal_type(generic_context(C[int].generic_method))  # revealed: tuple[Self@generic_method, U@generic_method]

c: C[int] = C[int]()
reveal_type(c.generic_method(1, "string"))  # revealed: Literal["string"]
reveal_type(generic_context(c))  # revealed: None
reveal_type(generic_context(c.method))  # revealed: tuple[Self@method]
reveal_type(generic_context(c.generic_method))  # revealed: tuple[Self@generic_method, U@generic_method]

Specializations propagate

In a specialized generic alias, the specialization is applied to the attributes and methods of the class.

from typing_extensions import Generic, TypeVar, Protocol

T = TypeVar("T")
U = TypeVar("U")

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

class C(Generic[T, U]):
    x: T
    y: U

    def method1(self) -> T:
        return self.x

    def method2(self) -> U:
        return self.y

    def method3(self) -> LinkedList[T]:
        return LinkedList[T]()

c = C[int, str]()
reveal_type(c.x)  # revealed: int
reveal_type(c.y)  # revealed: str
reveal_type(c.method1())  # revealed: int
reveal_type(c.method2())  # revealed: str
reveal_type(c.method3())  # revealed: LinkedList[int]

class SomeProtocol(Protocol[T]):
    x: T

class Foo(Generic[T]):
    x: T

class D(Generic[T, U]):
    x: T
    y: U

    def method1(self) -> T:
        return self.x

    def method2(self) -> U:
        return self.y

    def method3(self) -> SomeProtocol[T]:
        return Foo()

d = D[int, str]()
reveal_type(d.x)  # revealed: int
reveal_type(d.y)  # revealed: str
reveal_type(d.method1())  # revealed: int
reveal_type(d.method2())  # revealed: str
reveal_type(d.method3())  # revealed: SomeProtocol[int]
reveal_type(d.method3().x)  # revealed: int

When a method is overloaded, the specialization is applied to all overloads.

from typing_extensions import overload, Generic, TypeVar

S = TypeVar("S")

class WithOverloadedMethod(Generic[T]):
    @overload
    def method(self, x: T) -> T: ...
    @overload
    def method(self, x: S) -> S | T: ...
    def method(self, x: S | T) -> S | T:
        return x

# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int]
reveal_type(WithOverloadedMethod[int].method)

Cyclic class definitions

F-bounded quantification

A class can use itself as the type parameter of one of its superclasses. (This is also known as the curiously recurring template pattern or F-bounded quantification.)

In a stub file

Here, Sub is not a generic class, since it fills its superclass's type parameter (with itself).

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class Base(Generic[T]): ...
class Sub(Base[Sub]): ...

reveal_type(Sub)  # revealed: <class 'Sub'>

With string forward references

A similar case can work in a non-stub file, if forward references are stringified:

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class Base(Generic[T]): ...
class Sub(Base["Sub"]): ...

reveal_type(Sub)  # revealed: <class 'Sub'>

Without string forward references

In a non-stub file, without stringified forward references, this raises a NameError:

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

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

# error: [unresolved-reference]
class Sub(Base[Sub]): ...

Cyclic inheritance as a generic parameter

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

class Derived(list[Derived[T]], Generic[T]): ...

Direct cyclic inheritance

Inheritance that would result in a cyclic MRO is detected as an error.

from typing_extensions import Generic, TypeVar

T = TypeVar("T")

# error: [unresolved-reference]
class C(C, Generic[T]): ...

# error: [unresolved-reference]
class D(D[int], Generic[T]): ...