diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 58ec406333..161c222c4c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -752,6 +752,36 @@ b_container = ClassContainer[B](B) a_instance: A = use_a_class_container(b_container) # This should work ``` +## TypeIs + +```toml +[environment] +python-version = "3.13" +``` + +`TypeIs[T]` is invariant in `T`. See the [typing spec][typeis-spec] for a justification. + +```py +from typing import TypeIs +from ty_extensions import is_assignable_to, is_subtype_of, static_assert + +class A: + pass + +class B(A): + pass + +class C[T]: + def check(x: object) -> TypeIs[T]: + # this is a bad check, but we only care about it type-checking + return False + +static_assert(not is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +``` + ## Inheriting from generic classes with inferred variance When inheriting from a generic class with our type variable substituted in, we count its occurrences @@ -837,3 +867,4 @@ static_assert(is_subtype_of(DerivedContravariant[A], DerivedContravariant[B])) [linear-time-variance-talk]: https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s [spec]: https://typing.python.org/en/latest/spec/generics.html#variance +[typeis-spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 13e1a62278..bf68255901 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6620,6 +6620,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> { .map(|ty| ty.variance_of(db, typevar)) .collect(), Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, typevar), + Type::TypeIs(type_is_type) => type_is_type.variance_of(db, typevar), Type::Dynamic(_) | Type::Never | Type::WrapperDescriptor(_) @@ -6640,7 +6641,6 @@ impl<'db> VarianceInferable<'db> for Type<'db> { | Type::BoundSuper(_) | Type::TypeVar(_) | Type::NonInferableTypeVar(_) - | Type::TypeIs(_) | Type::TypedDict(_) | Type::TypeAlias(_) => TypeVarVariance::Bivariant, }; @@ -10956,6 +10956,16 @@ impl<'db> TypeIsType<'db> { } } +impl<'db> VarianceInferable<'db> for TypeIsType<'db> { + // See the [typing spec] on why `TypeIs` is invariant in its type. + // [typing spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + self.return_type(db) + .with_polarity(TypeVarVariance::Invariant) + .variance_of(db, typevar) + } +} + /// Walk the MRO of this class and return the last class just before the specified known base. /// This can be used to determine upper bounds for `Self` type variables on methods that are /// being added to the given class.