ruff/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md
Carl Meyer a176c1ac80
[red-knot] use fixpoint iteration for all cycles (#14029)
Pulls in the latest Salsa main branch, which supports fixpoint
iteration, and uses it to handle all query cycles.

With this, we no longer need to skip any corpus files to avoid panics.

Latest perf results show a 6% incremental and 1% cold-check regression.
This is not a "no cycles" regression, as tomllib and typeshed do trigger
some definition cycles (previously handled by our old
`infer_definition_types` fallback to `Unknown`). We don't currently have
a benchmark we can use to measure the pure no-cycles regression, though
I expect there would still be some regression; the fixpoint iteration
feature in Salsa does add some overhead even for non-cyclic queries.

I think this regression is within the reasonable range for this feature.
We can do further optimization work later, but I don't think it's the
top priority right now. So going ahead and acknowledging the regression
on CodSpeed.

Mypy primer is happy, so this doesn't regress anything on our
currently-checked projects. I expect it probably unlocks adding a number
of new projects to our ecosystem check that previously would have
panicked.

Fixes #13792
Fixes #14672
2025-03-12 12:41:40 +00:00

3.9 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]

string_annotation.py:

class Base[T]: ...

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

reveal_type(Sub)  # revealed: Literal[Sub]

bare_annotation.py:

class Base[T]: ...

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