ruff/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md
Douglas Creager 06cd249a9b
Some checks are pending
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
[ty] Track different uses of legacy typevars, including context when rendering typevars (#19604)
This PR introduces a few related changes:

- We now keep track of each time a legacy typevar is bound in a
different generic context (e.g. class, function), and internally create
a new `TypeVarInstance` for each usage. This means the rest of the code
can now assume that salsa-equivalent `TypeVarInstance`s refer to the
same typevar, even taking into account that legacy typevars can be used
more than once.

- We also go ahead and track the binding context of PEP 695 typevars.
That's _much_ easier to track since we have the binding context right
there during type inference.

- With that in place, we can now include the name of the binding context
when rendering typevars (e.g. `T@f` instead of `T`)
2025-08-01 12:20:32 -04:00

5.6 KiB

Legacy type variables

The tests in this file focus on how type variables are defined using the legacy notation. Most uses of type variables are tested in other files in this directory; we do not duplicate every test for both type variable syntaxes.

Unless otherwise specified, all quotations come from the Generics section of the typing spec.

Type variables

Defining legacy type variables

Generics can be parameterized by using a factory available in typing called TypeVar.

This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available in newer Python releases.

from typing import TypeVar

T = TypeVar("T")
reveal_type(type(T))  # revealed: <class 'TypeVar'>
reveal_type(T)  # revealed: typing.TypeVar
reveal_type(T.__name__)  # revealed: Literal["T"]

Directly assigned to a variable

A TypeVar() expression must always directly be assigned to a variable (it should not be used as part of a larger expression).

from typing import TypeVar

T = TypeVar("T")
# TODO: no error
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")

# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
# error: [invalid-type-form] "Function calls are not allowed in type expressions"
TestList = list[TypeVar("W")]

TypeVar parameter must match variable name

The argument to TypeVar() must be a string equal to the variable name to which it is assigned.

from typing import TypeVar

# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
T = TypeVar("Q")

No redefinition

Type variables must not be redefined.

from typing import TypeVar

T = TypeVar("T")

# TODO: error
T = TypeVar("T")

Type variables with a default

Note that the __default__ property is only available in Python ≥3.13.

[environment]
python-version = "3.13"
from typing import TypeVar

T = TypeVar("T", default=int)
reveal_type(T.__default__)  # revealed: int
reveal_type(T.__bound__)  # revealed: None
reveal_type(T.__constraints__)  # revealed: tuple[()]

S = TypeVar("S")
reveal_type(S.__default__)  # revealed: NoDefault

Using other typevars as a default

from typing import Generic, TypeVar, Union

T = TypeVar("T")
U = TypeVar("U", default=T)
V = TypeVar("V", default=Union[T, U])

class Valid(Generic[T, U, V]): ...

reveal_type(Valid())  # revealed: Valid[Unknown, Unknown, Unknown]
reveal_type(Valid[int]())  # revealed: Valid[int, int, int]
reveal_type(Valid[int, str]())  # revealed: Valid[int, str, int | str]
reveal_type(Valid[int, str, None]())  # revealed: Valid[int, str, None]

# TODO: error, default value for U isn't available in the generic context
class Invalid(Generic[U]): ...

Type variables with an upper bound

from typing import TypeVar

T = TypeVar("T", bound=int)
reveal_type(T.__bound__)  # revealed: int
reveal_type(T.__constraints__)  # revealed: tuple[()]

S = TypeVar("S")
reveal_type(S.__bound__)  # revealed: None

Type variables with constraints

from typing import TypeVar

T = TypeVar("T", int, str)
reveal_type(T.__constraints__)  # revealed: tuple[int, str]

S = TypeVar("S")
reveal_type(S.__constraints__)  # revealed: tuple[()]

Cannot have only one constraint

TypeVar supports constraining parametric types to a fixed set of possible types...There should be at least two constraints, if any; specifying a single constraint is disallowed.

from typing import TypeVar

# TODO: error: [invalid-type-variable-constraints]
T = TypeVar("T", int)

Cannot be both covariant and contravariant

To facilitate the declaration of container types where covariant or contravariant type checking is acceptable, type variables accept keyword arguments covariant=True or contravariant=True. At most one of these may be passed.

from typing import TypeVar

# error: [invalid-legacy-type-variable]
T = TypeVar("T", covariant=True, contravariant=True)

Variance parameters must be unambiguous

from typing import TypeVar

def cond() -> bool:
    return True

# error: [invalid-legacy-type-variable]
T = TypeVar("T", covariant=cond())

# error: [invalid-legacy-type-variable]
U = TypeVar("U", contravariant=cond())

Callability

A typevar bound to a Callable type is callable:

from typing import Callable, TypeVar

T = TypeVar("T", bound=Callable[[], int])

def bound(f: T):
    reveal_type(f)  # revealed: T@bound
    reveal_type(f())  # revealed: int

Same with a constrained typevar, as long as all constraints are callable:

T = TypeVar("T", Callable[[], int], Callable[[], str])

def constrained(f: T):
    reveal_type(f)  # revealed: T@constrained
    reveal_type(f())  # revealed: int | str

Meta-type

The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the meta-types of the constraints:

from typing import TypeVar

T_normal = TypeVar("T_normal")

def normal(x: T_normal):
    reveal_type(type(x))  # revealed: type

T_bound_object = TypeVar("T_bound_object", bound=object)

def bound_object(x: T_bound_object):
    reveal_type(type(x))  # revealed: type

T_bound_int = TypeVar("T_bound_int", bound=int)

def bound_int(x: T_bound_int):
    reveal_type(type(x))  # revealed: type[int]

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

def constrained(x: T_constrained):
    reveal_type(type(x))  # revealed: type[int] | type[str]