Skip to content

Commit 2502ff7

Browse files
[ty] Make TypeIs invariant in its type argument (#20428)
## Summary What it says on the tin. See the [typing spec](https://docs.python.org/3/library/typing.html#typing.TypeIs) for justification. ## Test Plan Add more tests to PEP 695 `variance.md` suite.
1 parent 144373f commit 2502ff7

File tree

2 files changed

+42
-1
lines changed

2 files changed

+42
-1
lines changed

crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,36 @@ b_container = ClassContainer[B](B)
752752
a_instance: A = use_a_class_container(b_container) # This should work
753753
```
754754

755+
## TypeIs
756+
757+
```toml
758+
[environment]
759+
python-version = "3.13"
760+
```
761+
762+
`TypeIs[T]` is invariant in `T`. See the [typing spec][typeis-spec] for a justification.
763+
764+
```py
765+
from typing import TypeIs
766+
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
767+
768+
class A:
769+
pass
770+
771+
class B(A):
772+
pass
773+
774+
class C[T]:
775+
def check(x: object) -> TypeIs[T]:
776+
# this is a bad check, but we only care about it type-checking
777+
return False
778+
779+
static_assert(not is_subtype_of(C[B], C[A]))
780+
static_assert(not is_subtype_of(C[A], C[B]))
781+
static_assert(not is_assignable_to(C[B], C[A]))
782+
static_assert(not is_assignable_to(C[A], C[B]))
783+
```
784+
755785
## Inheriting from generic classes with inferred variance
756786

757787
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]))
837867

838868
[linear-time-variance-talk]: https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s
839869
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
870+
[typeis-spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis

crates/ty_python_semantic/src/types.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6620,6 +6620,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
66206620
.map(|ty| ty.variance_of(db, typevar))
66216621
.collect(),
66226622
Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, typevar),
6623+
Type::TypeIs(type_is_type) => type_is_type.variance_of(db, typevar),
66236624
Type::Dynamic(_)
66246625
| Type::Never
66256626
| Type::WrapperDescriptor(_)
@@ -6640,7 +6641,6 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
66406641
| Type::BoundSuper(_)
66416642
| Type::TypeVar(_)
66426643
| Type::NonInferableTypeVar(_)
6643-
| Type::TypeIs(_)
66446644
| Type::TypedDict(_)
66456645
| Type::TypeAlias(_) => TypeVarVariance::Bivariant,
66466646
};
@@ -10956,6 +10956,16 @@ impl<'db> TypeIsType<'db> {
1095610956
}
1095710957
}
1095810958

10959+
impl<'db> VarianceInferable<'db> for TypeIsType<'db> {
10960+
// See the [typing spec] on why `TypeIs` is invariant in its type.
10961+
// [typing spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis
10962+
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
10963+
self.return_type(db)
10964+
.with_polarity(TypeVarVariance::Invariant)
10965+
.variance_of(db, typevar)
10966+
}
10967+
}
10968+
1095910969
/// Walk the MRO of this class and return the last class just before the specified known base.
1096010970
/// This can be used to determine upper bounds for `Self` type variables on methods that are
1096110971
/// being added to the given class.

0 commit comments

Comments
 (0)