[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 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 ### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13. Note that the `__default__` property is only available in Python ≥3.13.

View File

@ -98,6 +98,26 @@ def f[T: (int,)]():
pass 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 ## Invalid uses
Note that many of the invalid uses of legacy typevars do not apply to PEP 695 typevars, since the 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], list_or_tuple_legacy: ListOrTupleLegacy[int],
my_callable: MyCallable[[str, bytes], int], my_callable: MyCallable[[str, bytes], int],
annotated_int: AnnotatedType[int], annotated_int: AnnotatedType[int],
# error: [invalid-type-form] "A type variable itself cannot be specialized"
transparent_alias: TransparentAlias[int], transparent_alias: TransparentAlias[int],
optional_int: MyOptional[int], optional_int: MyOptional[int],
): ):
@ -427,7 +428,7 @@ def _(
reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
reveal_type(my_callable) # revealed: (str, bytes, /) -> int reveal_type(my_callable) # revealed: (str, bytes, /) -> int
reveal_type(annotated_int) # revealed: 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 reveal_type(optional_int) # revealed: int | None
``` ```

View File

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

View File

@ -5866,6 +5866,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}; };
if is_pep_613_type_alias { 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( self.add_declaration_with_binding(
target.into(), target.into(),
definition, definition,

View File

@ -16,8 +16,8 @@ use crate::types::tuple::{TupleSpecBuilder, TupleType};
use crate::types::{ use crate::types::{
BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass, BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass,
KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType,
Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder,
any_over_type, todo_type, UnionType, any_over_type, todo_type,
}; };
/// Type expressions /// Type expressions
@ -995,8 +995,26 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
} }
Type::unknown() 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) 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(_) KnownInstanceType::UnionType(_)