From 80bf852232534b9295eb44f5ccf14a4db2a14e56 Mon Sep 17 00:00:00 2001 From: cjc0013 Date: Tue, 30 Jun 2026 14:47:20 -0400 Subject: [PATCH 1/2] Fix TypeIs narrowing for covariant generic Any --- mypy/subtypes.py | 38 ++++++++++++++++++++++++++++++++ test-data/unit/check-typeis.test | 22 ++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 259bb3791deaf..59d5cf4550b60 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2203,9 +2203,47 @@ def restrict_subtype_away(t: Type, s: Type, *, consider_runtime_isinstance: bool return UninhabitedType() if is_proper_subtype(t, s, ignore_promotions=True, erase_instances=True): return UninhabitedType() + if covers_via_declared_variance(t, s): + return UninhabitedType() return t +def covers_via_declared_variance(item: Type, supertype: Type) -> bool: + """Is item covered by supertype when generic arguments follow declared variance?""" + item = get_proper_type(item) + supertype = get_proper_type(supertype) + if not isinstance(item, Instance) or not isinstance(supertype, Instance): + return False + if not item.type.has_base(supertype.type.fullname): + return False + + item = map_instance_to_supertype(item, supertype.type) + if len(item.args) != len(supertype.args): + return False + + tried_infer = False + type_params = zip(item.args, supertype.args, supertype.type.defn.type_vars) + for item_arg, supertype_arg, tvar in type_params: + if isinstance(tvar, TypeVarType): + if tvar.variance == VARIANCE_NOT_READY and not tried_infer: + infer_class_variances(supertype.type) + tried_infer = True + variance = tvar.variance + else: + variance = COVARIANT + + if variance == COVARIANT or variance == VARIANCE_NOT_READY: + if not is_subtype(item_arg, supertype_arg, ignore_promotions=True): + return False + elif variance == CONTRAVARIANT: + if not is_subtype(supertype_arg, item_arg, ignore_promotions=True): + return False + elif not is_same_type(item_arg, supertype_arg): + return False + + return True + + def covers_at_runtime(item: Type, supertype: Type) -> bool: """Will isinstance(item, supertype) always return True at runtime?""" item = get_proper_type(item) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 65ee837452f5b..aa3df7d988e81 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -541,6 +541,28 @@ def test(x: List[Any]) -> None: g(reveal_type(x)) # N: Revealed type is "builtins.list[builtins.str] | __main__." [builtins fixtures/tuple.pyi] +[case testTypeIsNegativeNarrowCovariantGenericAny] +from typing_extensions import TypeIs +from typing import Any, Generic, TypeVar + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) + +class Box(Generic[T]): ... +class Source(Generic[T_co]): ... + +def is_str_box(x: object) -> TypeIs[Box[str]]: ... +def is_source(x: object) -> TypeIs[Source[object]]: ... + +def keep_invariant(x: Box[Any]) -> None: + assert not is_str_box(x) + reveal_type(x) # N: Revealed type is "__main__.Box[Any]" + +def narrow_covariant(x: int | Source[Any]) -> None: + assert not is_source(x) + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + [case testTypeIsMultipleCondition] from typing_extensions import TypeIs from typing import Any, List From 660d681924eed3472ae390c472b4ca7c53d0061f Mon Sep 17 00:00:00 2001 From: cjc0013 Date: Mon, 6 Jul 2026 15:04:29 -0400 Subject: [PATCH 2/2] Use variance-aware subtype coverage for TypeIs --- mypy/subtypes.py | 76 ++++++++++++++++---------------- test-data/unit/check-typeis.test | 9 +++- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 59d5cf4550b60..fbbae90b1f14c 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -101,6 +101,9 @@ def __init__( # Proper subtype flags erase_instances: bool = False, keep_erased_types: bool = False, + # Use ordinary compatibility for variant type arguments while preserving + # exact/proper checks for invariant arguments. + allow_any_in_variant_positions: bool = False, options: Options | None = None, ) -> None: self.ignore_type_params = ignore_type_params @@ -110,15 +113,30 @@ def __init__( self.ignore_promotions = ignore_promotions self.erase_instances = erase_instances self.keep_erased_types = keep_erased_types + self.allow_any_in_variant_positions = allow_any_in_variant_positions self.options = options + def non_proper_context(self) -> SubtypeContext: + return SubtypeContext( + ignore_type_params=self.ignore_type_params, + ignore_pos_arg_names=self.ignore_pos_arg_names, + ignore_declared_variance=self.ignore_declared_variance, + always_covariant=self.always_covariant, + ignore_promotions=self.ignore_promotions, + options=self.options, + ) + def check_context(self, proper_subtype: bool) -> None: # Historically proper and non-proper subtypes were defined using different helpers # and different visitors. Check if flag values are such that we definitely support. if proper_subtype: assert not self.ignore_pos_arg_names and not self.ignore_declared_variance else: - assert not self.erase_instances and not self.keep_erased_types + assert ( + not self.erase_instances + and not self.keep_erased_types + and not self.allow_any_in_variant_positions + ) def is_subtype( @@ -197,6 +215,7 @@ def is_proper_subtype( ignore_promotions: bool = False, erase_instances: bool = False, keep_erased_types: bool = False, + allow_any_in_variant_positions: bool = False, ) -> bool: """Is left a proper subtype of right? @@ -206,6 +225,8 @@ def is_proper_subtype( If erase_instances is True, erase left instance *after* mapping it to supertype (this is useful for runtime isinstance() checks). If keep_erased_types is True, do not consider ErasedType a subtype of all types (used by type inference against unions). + If allow_any_in_variant_positions is True, variant type arguments use ordinary subtype + compatibility while invariant arguments still use proper/exact checks. """ if left == right: return True @@ -214,10 +235,14 @@ def is_proper_subtype( ignore_promotions=ignore_promotions, erase_instances=erase_instances, keep_erased_types=keep_erased_types, + allow_any_in_variant_positions=allow_any_in_variant_positions, ) else: assert ( - not ignore_promotions and not erase_instances and not keep_erased_types + not ignore_promotions + and not erase_instances + and not keep_erased_types + and not allow_any_in_variant_positions ), "Don't pass both context and individual flags" if type_state.is_assumed_proper_subtype(left, right): return True @@ -390,11 +415,19 @@ def check_type_parameter( # avoid these cases altogether. if variance == COVARIANT or variance == VARIANCE_NOT_READY: if proper_subtype: + if subtype_context.allow_any_in_variant_positions: + return is_subtype( + left, right, subtype_context=subtype_context.non_proper_context() + ) return is_proper_subtype(left, right, subtype_context=subtype_context) else: return is_subtype(left, right, subtype_context=subtype_context) elif variance == CONTRAVARIANT: if proper_subtype: + if subtype_context.allow_any_in_variant_positions: + return is_subtype( + right, left, subtype_context=subtype_context.non_proper_context() + ) return is_proper_subtype(right, left, subtype_context=subtype_context) else: return is_subtype(right, left, subtype_context=subtype_context) @@ -440,6 +473,7 @@ def build_subtype_kind(subtype_context: SubtypeContext, proper_subtype: bool) -> subtype_context.ignore_promotions, subtype_context.erase_instances, subtype_context.keep_erased_types, + subtype_context.allow_any_in_variant_positions, ) def _is_subtype(self, left: Type, right: Type) -> bool: @@ -2203,47 +2237,11 @@ def restrict_subtype_away(t: Type, s: Type, *, consider_runtime_isinstance: bool return UninhabitedType() if is_proper_subtype(t, s, ignore_promotions=True, erase_instances=True): return UninhabitedType() - if covers_via_declared_variance(t, s): + if is_proper_subtype(t, s, ignore_promotions=True, allow_any_in_variant_positions=True): return UninhabitedType() return t -def covers_via_declared_variance(item: Type, supertype: Type) -> bool: - """Is item covered by supertype when generic arguments follow declared variance?""" - item = get_proper_type(item) - supertype = get_proper_type(supertype) - if not isinstance(item, Instance) or not isinstance(supertype, Instance): - return False - if not item.type.has_base(supertype.type.fullname): - return False - - item = map_instance_to_supertype(item, supertype.type) - if len(item.args) != len(supertype.args): - return False - - tried_infer = False - type_params = zip(item.args, supertype.args, supertype.type.defn.type_vars) - for item_arg, supertype_arg, tvar in type_params: - if isinstance(tvar, TypeVarType): - if tvar.variance == VARIANCE_NOT_READY and not tried_infer: - infer_class_variances(supertype.type) - tried_infer = True - variance = tvar.variance - else: - variance = COVARIANT - - if variance == COVARIANT or variance == VARIANCE_NOT_READY: - if not is_subtype(item_arg, supertype_arg, ignore_promotions=True): - return False - elif variance == CONTRAVARIANT: - if not is_subtype(supertype_arg, item_arg, ignore_promotions=True): - return False - elif not is_same_type(item_arg, supertype_arg): - return False - - return True - - def covers_at_runtime(item: Type, supertype: Type) -> bool: """Will isinstance(item, supertype) always return True at runtime?""" item = get_proper_type(item) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index aa3df7d988e81..9cea4df0522de 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -542,17 +542,20 @@ def test(x: List[Any]) -> None: [builtins fixtures/tuple.pyi] [case testTypeIsNegativeNarrowCovariantGenericAny] -from typing_extensions import TypeIs +from typing_extensions import Never, TypeIs from typing import Any, Generic, TypeVar T = TypeVar("T") T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) class Box(Generic[T]): ... class Source(Generic[T_co]): ... +class Sink(Generic[T_contra]): ... def is_str_box(x: object) -> TypeIs[Box[str]]: ... def is_source(x: object) -> TypeIs[Source[object]]: ... +def is_sink(x: object) -> TypeIs[Sink[Never]]: ... def keep_invariant(x: Box[Any]) -> None: assert not is_str_box(x) @@ -561,6 +564,10 @@ def keep_invariant(x: Box[Any]) -> None: def narrow_covariant(x: int | Source[Any]) -> None: assert not is_source(x) reveal_type(x) # N: Revealed type is "builtins.int" + +def narrow_contravariant(x: int | Sink[Any]) -> None: + assert not is_sink(x) + reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] [case testTypeIsMultipleCondition]