[ty] disallow explicit specialization of type variables themselves (#21938)

## Summary

This PR makes explicit specialization of a type variable itself an
error, and the result of the specialization is `Unknown`.

The change also fixes https://github.com/astral-sh/ty/issues/1794.

## Test Plan

mdtests updated
new corpus test

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Shunsuke Shibayama 2025-12-13 08:49:20 +09:00 committed by GitHub
parent 5a2aba237b
commit e19c050386
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 103 additions and 6 deletions

View File

@ -0,0 +1,7 @@
from typing import TypeAlias, TypeVar
T = TypeVar("T", bound="A[0]")
A: TypeAlias = T
def _(x: A):
if x:
pass

View File

@ -0,0 +1,3 @@
def _[T: T[0]](x: T):
if x:
pass

View File

@ -104,6 +104,34 @@ S = TypeVar("S", **{"bound": int})
reveal_type(S) # revealed: TypeVar
```
### No explicit specialization
A type variable itself cannot be explicitly specialized; the result of the specialization is
`Unknown`. However, generic PEP 613 type aliases that point to type variables can be explicitly
specialized.
```py
from typing import TypeVar, TypeAlias
T = TypeVar("T")
ImplicitPositive = T
Positive: TypeAlias = T
def _(
# error: [invalid-type-form] "A type variable itself cannot be specialized"
a: T[int],
# error: [invalid-type-form] "A type variable itself cannot be specialized"
b: T[T],
# error: [invalid-type-form] "A type variable itself cannot be specialized"
c: ImplicitPositive[int],
d: Positive[int],
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: int
```
### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13.

View File

@ -98,6 +98,26 @@ def f[T: (int,)]():
pass
```
### No explicit specialization
A type variable itself cannot be explicitly specialized; the result of the specialization is
`Unknown`. However, generic type aliases that point to type variables can be explicitly specialized.
```py
type Positive[T] = T
def _[T](
# error: [invalid-type-form] "A type variable itself cannot be specialized"
a: T[int],
# error: [invalid-type-form] "A type variable itself cannot be specialized"
b: T[T],
c: Positive[int],
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: int
```
## Invalid uses
Note that many of the invalid uses of legacy typevars do not apply to PEP 695 typevars, since the

View File

@ -414,6 +414,7 @@ def _(
list_or_tuple_legacy: ListOrTupleLegacy[int],
my_callable: MyCallable[[str, bytes], int],
annotated_int: AnnotatedType[int],
# error: [invalid-type-form] "A type variable itself cannot be specialized"
transparent_alias: TransparentAlias[int],
optional_int: MyOptional[int],
):
@ -427,7 +428,7 @@ def _(
reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
reveal_type(my_callable) # revealed: (str, bytes, /) -> int
reveal_type(annotated_int) # revealed: int
reveal_type(transparent_alias) # revealed: int
reveal_type(transparent_alias) # revealed: Unknown
reveal_type(optional_int) # revealed: int | None
```

View File

@ -7994,7 +7994,7 @@ impl<'db> Type<'db> {
) {
let matching_typevar = |bound_typevar: &BoundTypeVarInstance<'db>| {
match bound_typevar.typevar(db).kind(db) {
TypeVarKind::Legacy | TypeVarKind::TypingSelf
TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::TypingSelf
if binding_context.is_none_or(|binding_context| {
bound_typevar.binding_context(db)
== BindingContext::Definition(binding_context)
@ -9472,6 +9472,8 @@ pub enum TypeVarKind {
ParamSpec,
/// `def foo[**P]() -> None: ...`
Pep695ParamSpec,
/// `Alias: typing.TypeAlias = T`
Pep613Alias,
}
impl TypeVarKind {

View File

@ -5866,6 +5866,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
if is_pep_613_type_alias {
let inferred_ty =
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = inferred_ty {
let identity = TypeVarIdentity::new(
self.db(),
typevar.identity(self.db()).name(self.db()),
typevar.identity(self.db()).definition(self.db()),
TypeVarKind::Pep613Alias,
);
Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
self.db(),
identity,
typevar._bound_or_constraints(self.db()),
typevar.explicit_variance(self.db()),
typevar._default(self.db()),
)))
} else {
inferred_ty
};
self.add_declaration_with_binding(
target.into(),
definition,

View File

@ -16,8 +16,8 @@ use crate::types::tuple::{TupleSpecBuilder, TupleType};
use crate::types::{
BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass,
KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType,
Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType,
any_over_type, todo_type,
Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder,
UnionType, any_over_type, todo_type,
};
/// Type expressions
@ -995,8 +995,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
Type::unknown()
}
KnownInstanceType::TypeVar(_) => {
KnownInstanceType::TypeVar(typevar) => {
// The type variable designated as a generic type alias by `typing.TypeAlias` can be explicitly specialized.
// ```py
// from typing import TypeVar, TypeAlias
// T = TypeVar('T')
// Annotated: TypeAlias = T
// _: Annotated[int] = 1 # valid
// ```
if typevar.identity(self.db()).kind(self.db()) == TypeVarKind::Pep613Alias {
self.infer_explicit_type_alias_specialization(subscript, value_ty, false)
} else {
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
{
builder.into_diagnostic(format_args!(
"A type variable itself cannot be specialized",
));
}
Type::unknown()
}
}
KnownInstanceType::UnionType(_)