ruff/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md
Shunsuke Shibayama ee51c2a389
[red-knot] fix ordering of ClassDef semantic index building (#16915)
## Summary

From #16861

This PR fixes the incorrect `ClassDef` handling of
`SemanticIndexBuilder::visit_stmt`, which fixes some of the incorrect
behavior of referencing the class itself in the class scope (a complete
fix requires a different fix, which will be done in the another PR).

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-03-23 13:23:12 +00:00

4.3 KiB

Generic classes

PEP 695 syntax

TODO: Add a red_knot_extension function that asserts whether a function or class is generic.

This is a generic class defined using PEP 695 syntax:

class C[T]: ...

A class that inherits from a generic class, and fills its type parameters with typevars, is generic:

# TODO: no error
# error: [non-subscriptable]
class D[U](C[U]): ...

A class that inherits from a generic class, but fills its type parameters with concrete types, is not generic:

# TODO: no error
# error: [non-subscriptable]
class E(C[int]): ...

A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly uses the default value for the typevar. In this case, that default type is Unknown, so F inherits from C[Unknown] and is not itself generic.

class F(C): ...

Legacy syntax

This is a generic class defined using the legacy syntax:

from typing import Generic, TypeVar

T = TypeVar("T")

# TODO: no error
# error: [invalid-base]
class C(Generic[T]): ...

A class that inherits from a generic class, and fills its type parameters with typevars, is generic.

class D(C[T]): ...

(Examples E and F from above do not have analogues in the legacy syntax.)

Inferring generic class parameters

The type parameter can be specified explicitly:

class C[T]:
    x: T

# TODO: no error
# TODO: revealed: C[int]
# error: [non-subscriptable]
reveal_type(C[int]())  # revealed: C

We can infer the type parameter from a type context:

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

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

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

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:

class D[T = int]: ...

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

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

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

If the type of a constructor parameter is a class typevar, we can use that to infer the type parameter:

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

# TODO: revealed: E[int] or E[Literal[1]]
reveal_type(E(1))  # revealed: E

The types inferred from a type context and from a constructor parameter must be consistent with each other:

# TODO: error
wrong_innards: E[int] = E("five")

Generic subclass

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

class Base[T]:
    x: T | None = None

# TODO: no error
# error: [non-subscriptable]
class Sub[U](Base[U]): ...

# TODO: no error
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Base[int].x)  # revealed: T | None
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Sub[int].x)  # revealed: T | None

Cyclic class definition

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.)

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

stub.pyi:

class Base[T]: ...
# TODO: no error
# error: [non-subscriptable]
class Sub(Base[Sub]): ...

reveal_type(Sub)  # revealed: Literal[Sub]

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

string_annotation.py:

class Base[T]: ...

# TODO: no error
# error: [non-subscriptable]
class Sub(Base["Sub"]): ...

reveal_type(Sub)  # revealed: Literal[Sub]

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

bare_annotation.py:

class Base[T]: ...

# TODO: the unresolved-reference error is correct, the non-subscriptable is not
# error: [non-subscriptable]
# error: [unresolved-reference]
class Sub(Base[Sub]): ...

Another cyclic case

# TODO no error (generics)
# error: [invalid-base]
class Derived[T](list[Derived[T]]): ...