[ty] Support infer_variance for legacy TypeVar (#24930)

## Summary

See:
https://github.com/astral-sh/ruff/pull/24927#discussion_r3162016951.
This commit is contained in:
Charlie Marsh
2026-04-29 12:49:34 -04:00
committed by GitHub
parent 1a794b966a
commit e6528502ba
2 changed files with 113 additions and 15 deletions
@@ -435,6 +435,93 @@ from typing import TypeVar
T = TypeVar("T", covariant=True, contravariant=True)
```
### Infer variance
For a `TypeVar` with `infer_variance=True`, we infer covariance when the type variable only appears
in return positions, contravariance when it only appears in parameter positions, and invariance when
it appears in both positions.
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Generic, TypeVar
OutT = TypeVar("OutT", infer_variance=True)
class Source(Generic[OutT]):
def get(self) -> OutT:
raise NotImplementedError
source_int: Source[int] = Source[object]() # error: [invalid-assignment]
source_obj: Source[object] = Source[int]()
InT = TypeVar("InT", infer_variance=True)
class Sink(Generic[InT]):
def send(self, value: InT) -> None:
raise NotImplementedError
sink_obj: Sink[object] = Sink[int]() # error: [invalid-assignment]
sink_int: Sink[int] = Sink[object]()
```
Both assignments are errors when the type variable is inferred to be invariant:
```py
from typing import Generic, TypeVar
T = TypeVar("T", infer_variance=True)
class Box(Generic[T]):
value: T
box_int: Box[int] = Box[object]() # error: [invalid-assignment]
box_obj: Box[object] = Box[int]() # error: [invalid-assignment]
```
> A generic class that uses the traditional syntax may include combinations of type variables with
> explicit and inferred variance.
```py
from typing import Generic, TypeVar
ExplicitOutT = TypeVar("ExplicitOutT", covariant=True)
InferredInT = TypeVar("InferredInT", infer_variance=True)
class Mixed(Generic[ExplicitOutT, InferredInT]):
def get(self) -> ExplicitOutT:
raise NotImplementedError
def send(self, value: InferredInT) -> None:
raise NotImplementedError
mixed_covariant: Mixed[object, int] = Mixed[int, int]()
mixed_not_covariant: Mixed[int, int] = Mixed[object, int]() # error: [invalid-assignment]
mixed_contravariant: Mixed[int, int] = Mixed[int, object]()
mixed_not_contravariant: Mixed[int, object] = Mixed[int, int]() # error: [invalid-assignment]
```
Variance cannot be specified explicitly when variance inference is requested:
```py
from typing import TypeVar
# snapshot: invalid-legacy-type-variable
CovariantAndInferred = TypeVar("CovariantAndInferred", covariant=True, infer_variance=True)
```
```snapshot
error[invalid-legacy-type-variable]: A `TypeVar` cannot specify variance when `infer_variance=True`
--> src/mdtest_snippet.py:48:24
|
48 | CovariantAndInferred = TypeVar("CovariantAndInferred", covariant=True, infer_variance=True)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
```
### Boolean parameters must be unambiguous
```py
@@ -950,6 +950,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut default = None;
let mut covariant = false;
let mut contravariant = false;
let mut infer_variance = false;
let mut name_param_ty = None;
let mut name_param_node = None;
@@ -1042,18 +1043,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
kwarg,
);
}
// TODO support `infer_variance` in legacy TypeVars
if self
match self
.infer_expression(&kwarg.value, TypeContext::default())
.bool(db)
.is_ambiguous()
{
return error(
&self.context,
"The `infer_variance` parameter of `TypeVar` \
cannot have an ambiguous truthiness",
&kwarg.value,
);
Truthiness::AlwaysTrue => infer_variance = true,
Truthiness::AlwaysFalse => {}
Truthiness::Ambiguous => {
return error(
&self.context,
"The `infer_variance` parameter of `TypeVar` \
cannot have an ambiguous truthiness",
&kwarg.value,
);
}
}
}
name => {
@@ -1071,17 +1074,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
let variance = match (covariant, contravariant) {
(true, true) => {
let variance = match (covariant, contravariant, infer_variance) {
(true, true, _) => {
return error(
&self.context,
"A `TypeVar` cannot be both covariant and contravariant",
call_expr,
);
}
(true, false) => TypeVarVariance::Covariant,
(false, true) => TypeVarVariance::Contravariant,
(false, false) => TypeVarVariance::Invariant,
(true, false, true) | (false, true, true) => {
return error(
&self.context,
"A `TypeVar` cannot specify variance when `infer_variance=True`",
call_expr,
);
}
(true, false, false) => Some(TypeVarVariance::Covariant),
(false, true, false) => Some(TypeVarVariance::Contravariant),
(false, false, false) => Some(TypeVarVariance::Invariant),
(false, false, true) => None,
};
let Some(name_param_ty) = name_param_ty.or_else(|| {
@@ -1168,7 +1179,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
db,
identity,
bound_or_constraints,
Some(variance),
variance,
default,
)))
}