From 46da423a9a3ec49440431e9f266290851d2229d1 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Tue, 15 Jul 2025 18:47:39 +0200 Subject: [PATCH 01/14] fixed match-case against typing.Callable --- mypy/checkpattern.py | 10 ++++++++++ test-data/unit/check-python310.test | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 48840466f0d8..8543a6121b94 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -50,6 +50,7 @@ UninhabitedType, UnionType, UnpackType, + callable_with_ellipsis, find_unpack_in_list, get_proper_type, split_with_prefix_and_suffix, @@ -553,6 +554,15 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType: and isinstance(get_proper_type(type_info.type), AnyType) ): typ = type_info.type + elif ( + isinstance(type_info, Var) + and type_info.type is not None + and type_info.fullname == "typing.Callable" + ): + # Consider as `Callable[..., Any]` + fallback = self.chk.named_type("builtins.function") + any_type = AnyType(TypeOfAny.unannotated) + typ = callable_with_ellipsis(any_type, any_type, fallback) else: if isinstance(type_info, Var) and type_info.type is not None: name = type_info.type.str_with_options(self.options) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index bb8f038eb1eb..10840cd5a689 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1069,6 +1069,15 @@ match m: case Foo(): pass +[case testMatchClassPatternCallable] +from typing import Callable + +x: object + +match x: + case Callable() as fn: + reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" + [case testMatchClassPatternNestedGenerics] # From cpython test_patma.py x = [[{0: 0}]] From 5fbcbed9a72d59675cbafad714419362d3c53f56 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Wed, 16 Jul 2025 21:45:01 +0200 Subject: [PATCH 02/14] more precise type narrowing --- mypy/checkpattern.py | 6 ++++-- test-data/unit/check-python310.test | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 8543a6121b94..d27627f86619 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -559,10 +559,12 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType: and type_info.type is not None and type_info.fullname == "typing.Callable" ): - # Consider as `Callable[..., Any]` + # Create a `Callable[..., Any]` fallback = self.chk.named_type("builtins.function") any_type = AnyType(TypeOfAny.unannotated) - typ = callable_with_ellipsis(any_type, any_type, fallback) + fn_type = callable_with_ellipsis(any_type, ret_type=any_type, fallback=fallback) + # if typ is itself callable, use its own type, otherwise Callable[..,, Any] + typ = current_type if is_subtype(current_type, fn_type) else fn_type else: if isinstance(type_info, Var) and type_info.type is not None: name = type_info.type.str_with_options(self.options) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 10840cd5a689..968f70511b25 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1078,6 +1078,13 @@ match x: case Callable() as fn: reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" +def int_fun(x: int) -> int: return x+1 + +match int_fun: + case Callable() as fn: + reveal_type(fn) # N: Revealed type is "def (x: builtins.int) -> builtins.int" + + [case testMatchClassPatternNestedGenerics] # From cpython test_patma.py x = [[{0: 0}]] From a5c4ebe5a4f08161defb290e39a4be6efda2e89b Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 17 Jul 2025 08:23:14 +0200 Subject: [PATCH 03/14] improve narrowing + better tests --- mypy/checker.py | 7 +++++- mypy/checkpattern.py | 4 +--- test-data/unit/check-protocols.test | 5 +++-- test-data/unit/check-python310.test | 34 ++++++++++++++++++++++------- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7579c36a97d0..413de5515a18 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8015,11 +8015,16 @@ def conditional_types( # attempt to narrow anything. Instead, we broaden the expr to Any to # avoid false positives return proposed_type, default - elif not any( + elif not any( # handle concrete subtypes type_range.is_upper_bound for type_range in proposed_type_ranges ) and is_proper_subtype(current_type, proposed_type, ignore_promotions=True): # Expression is always of one of the types in proposed_type_ranges return default, UninhabitedType() + elif ( # handle structural subtypes + isinstance(proposed_type, CallableType) + or (isinstance(proposed_type, Instance) and proposed_type.type.runtime_protocol) + ) and is_subtype(current_type, proposed_type, ignore_promotions=True): + return default, UninhabitedType() elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): # Expression is never of any type in proposed_type_ranges return UninhabitedType(), default diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index d27627f86619..0585314e33f9 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -562,9 +562,7 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType: # Create a `Callable[..., Any]` fallback = self.chk.named_type("builtins.function") any_type = AnyType(TypeOfAny.unannotated) - fn_type = callable_with_ellipsis(any_type, ret_type=any_type, fallback=fallback) - # if typ is itself callable, use its own type, otherwise Callable[..,, Any] - typ = current_type if is_subtype(current_type, fn_type) else fn_type + typ = callable_with_ellipsis(any_type, ret_type=any_type, fallback=fallback) else: if isinstance(type_info, Var) and type_info.type is not None: name = type_info.type.str_with_options(self.options) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 79207c9aad56..4a17b323399b 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -1506,11 +1506,12 @@ class C: pass def f(x: P1) -> int: ... @overload def f(x: P2) -> str: ... -def f(x): +def f(x: object) -> object: if isinstance(x, P1): return P1.attr1 if isinstance(x, P2): # E: Only @runtime_checkable protocols can be used with instance and class checks - return P1.attr2 + return P2.attr2 + return None reveal_type(f(C1())) # N: Revealed type is "builtins.int" reveal_type(f(C2())) # N: Revealed type is "builtins.str" diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 968f70511b25..e136587db9dc 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1072,18 +1072,36 @@ match m: [case testMatchClassPatternCallable] from typing import Callable -x: object +def test_object(x: object) -> None: + match x: + case Callable() as fn: + reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" -match x: - case Callable() as fn: - reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" +def test_intfun(x: Callable[[int], int]) -> None: + match x: + case Callable() as fn: + reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" -def int_fun(x: int) -> int: return x+1 -match int_fun: - case Callable() as fn: - reveal_type(fn) # N: Revealed type is "def (x: builtins.int) -> builtins.int" +[case testMatchClassPatternCallbackProtocol] +from typing import Any, Callable +from typing_extensions import Protocol, runtime_checkable +@runtime_checkable +class AnyCallable(Protocol): + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + +def test_object(x: object) -> None: + match x: + case AnyCallable() as fn: + reveal_type(fn) # N: Revealed type is "__main__.AnyCallable" + +def test_intfun(x: Callable[[int], int]) -> None: + match x: + case AnyCallable() as fn: + reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" + +[builtins fixtures/dict.pyi] [case testMatchClassPatternNestedGenerics] # From cpython test_patma.py From d388ec9f8f8188aae4dc3f648cf7d9d2beb12754 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 17 Jul 2025 08:36:41 +0200 Subject: [PATCH 04/14] added test for plain protocols --- test-data/unit/check-python310.test | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index e136587db9dc..073c60184a15 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1103,6 +1103,30 @@ def test_intfun(x: Callable[[int], int]) -> None: [builtins fixtures/dict.pyi] + +[case testMatchClassPatternProtocol] +from typing import Any +from typing_extensions import Protocol, runtime_checkable + +class Proto(Protocol): + def foo(self, x: int, /) -> object: ... + +class Impl: + def foo(self, x: object, /) -> int: ... + +def test_object(x: object) -> None: + match x: + case Proto() as y: + reveal_type(y) # N: Revealed type is "__main__.Proto" + +def test_impl(x: Impl) -> None: + match x: + case Proto() as y: + reveal_type(y) # N: Revealed type is "__main__.Impl" + +[builtins fixtures/dict.pyi] + + [case testMatchClassPatternNestedGenerics] # From cpython test_patma.py x = [[{0: 0}]] From ab1e450d12b4a20dec3920368e28cc9a81e92f2b Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 17 Jul 2025 08:37:25 +0200 Subject: [PATCH 05/14] use `is_protocol` rather than `runtime_protocol` since the runtime-checkable test should be performed elsewhere. --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 413de5515a18..3ffcea52b04e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8022,7 +8022,7 @@ def conditional_types( return default, UninhabitedType() elif ( # handle structural subtypes isinstance(proposed_type, CallableType) - or (isinstance(proposed_type, Instance) and proposed_type.type.runtime_protocol) + or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol) ) and is_subtype(current_type, proposed_type, ignore_promotions=True): return default, UninhabitedType() elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): From 89f8350478aab1d48263cde1c36fdbe5de1113f9 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 17 Jul 2025 08:55:05 +0200 Subject: [PATCH 06/14] split Callback test into two. --- test-data/unit/check-python310.test | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 073c60184a15..c922544f0b70 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1077,7 +1077,7 @@ def test_object(x: object) -> None: case Callable() as fn: reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" -def test_intfun(x: Callable[[int], int]) -> None: +def test_impl(x: Callable[[int], int]) -> None: match x: case Callable() as fn: reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" @@ -1087,6 +1087,26 @@ def test_intfun(x: Callable[[int], int]) -> None: from typing import Any, Callable from typing_extensions import Protocol, runtime_checkable +@runtime_checkable +class FnProto(Protocol): + def __call__(self, x: int, /) -> object: ... + +def test_object(x: object) -> None: + match x: + case FnProto() as fn: + reveal_type(fn) # N: Revealed type is "__main__.FnProto" + +def test_impl(x: Callable[[int], int]) -> None: + match x: + case FnProto() as fn: + reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" + +[builtins fixtures/dict.pyi] + +[case testMatchClassPatternAnyCallableProtocol] +from typing import Any, Callable +from typing_extensions import Protocol, runtime_checkable + @runtime_checkable class AnyCallable(Protocol): def __call__(self, *args: Any, **kwargs: Any) -> Any: ... @@ -1096,7 +1116,7 @@ def test_object(x: object) -> None: case AnyCallable() as fn: reveal_type(fn) # N: Revealed type is "__main__.AnyCallable" -def test_intfun(x: Callable[[int], int]) -> None: +def test_impl(x: Callable[[int], int]) -> None: match x: case AnyCallable() as fn: reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" From c5d34c9a9a0f23e2e09b812846f0d0c5bd5e666a Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 17 Jul 2025 09:00:40 +0200 Subject: [PATCH 07/14] Improved callback test by checking both against concrete implementation and anonymous `Callable` --- test-data/unit/check-python310.test | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index c922544f0b70..279d06c9cb18 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1091,15 +1091,23 @@ from typing_extensions import Protocol, runtime_checkable class FnProto(Protocol): def __call__(self, x: int, /) -> object: ... +class FnImpl: + def __call__(self, x: object, /) -> int: ... + def test_object(x: object) -> None: match x: case FnProto() as fn: reveal_type(fn) # N: Revealed type is "__main__.FnProto" -def test_impl(x: Callable[[int], int]) -> None: +def test_impl(x: FnImpl) -> None: match x: case FnProto() as fn: - reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" + reveal_type(fn) # N: Revealed type is "__main__.FnImpl" + +def test_callable(x: Callable[[object], int]) -> None: + match x: + case FnProto() as fn: + reveal_type(fn) # N: Revealed type is "def (builtins.object) -> builtins.int" [builtins fixtures/dict.pyi] @@ -1111,15 +1119,23 @@ from typing_extensions import Protocol, runtime_checkable class AnyCallable(Protocol): def __call__(self, *args: Any, **kwargs: Any) -> Any: ... +class FnImpl: + def __call__(self, x: object, /) -> int: ... + def test_object(x: object) -> None: match x: case AnyCallable() as fn: reveal_type(fn) # N: Revealed type is "__main__.AnyCallable" -def test_impl(x: Callable[[int], int]) -> None: +def test_impl(x: FnImpl) -> None: match x: case AnyCallable() as fn: - reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" + reveal_type(fn) # N: Revealed type is "__main__.FnImpl" + +def test_callable(x: Callable[[object], int]) -> None: + match x: + case AnyCallable() as fn: + reveal_type(fn) # N: Revealed type is "def (builtins.object) -> builtins.int" [builtins fixtures/dict.pyi] From c4471a11d91494e1fb575a1bf97a69beec20926f Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 17 Jul 2025 09:21:58 +0200 Subject: [PATCH 08/14] improved testMatchClassPatternCallable with extra check --- test-data/unit/check-python310.test | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 279d06c9cb18..d3e14d783fac 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1072,15 +1072,23 @@ match m: [case testMatchClassPatternCallable] from typing import Callable +class FnImpl: + def __call__(self, x: object, /) -> int: ... + def test_object(x: object) -> None: match x: case Callable() as fn: reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" -def test_impl(x: Callable[[int], int]) -> None: +def test_impl(x: FnImpl) -> None: + match x: + case Callable() as fn: + reveal_type(fn) # N: Revealed type is "__main__.FnImpl" + +def test_callable(x: Callable[[object], int]) -> None: match x: case Callable() as fn: - reveal_type(fn) # N: Revealed type is "def (builtins.int) -> builtins.int" + reveal_type(fn) # N: Revealed type is "def (builtins.object) -> builtins.int" [case testMatchClassPatternCallbackProtocol] From 9a3e37479297c95d3ec2f6363f4006a0e35d9adc Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Thu, 17 Jul 2025 09:36:24 +0200 Subject: [PATCH 09/14] Fix narrowing when current_type is `Any`. --- mypy/checker.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3ffcea52b04e..c9a4b5ac9c21 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8024,7 +8024,15 @@ def conditional_types( isinstance(proposed_type, CallableType) or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol) ) and is_subtype(current_type, proposed_type, ignore_promotions=True): - return default, UninhabitedType() + yes_type = default + no_type = ( + None + if yes_type is None + else restrict_subtype_away( + current_type, yes_type, consider_runtime_isinstance=consider_runtime_isinstance + ) + ) + return yes_type, no_type elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): # Expression is never of any type in proposed_type_ranges return UninhabitedType(), default From 74db0aab2c322d9f963a107b0c1d003fddd8e9d8 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Sun, 20 Jul 2025 09:23:13 +0200 Subject: [PATCH 10/14] fixed return type using mesonbuild mypy-primer failure --- mypy/checker.py | 26 +++++++++++--------------- test-data/unit/check-isinstance.test | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c9a4b5ac9c21..7620bab10b2b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8015,24 +8015,20 @@ def conditional_types( # attempt to narrow anything. Instead, we broaden the expr to Any to # avoid false positives return proposed_type, default - elif not any( # handle concrete subtypes + elif not any( type_range.is_upper_bound for type_range in proposed_type_ranges - ) and is_proper_subtype(current_type, proposed_type, ignore_promotions=True): - # Expression is always of one of the types in proposed_type_ranges - return default, UninhabitedType() - elif ( # handle structural subtypes - isinstance(proposed_type, CallableType) - or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol) - ) and is_subtype(current_type, proposed_type, ignore_promotions=True): - yes_type = default - no_type = ( - None - if yes_type is None - else restrict_subtype_away( - current_type, yes_type, consider_runtime_isinstance=consider_runtime_isinstance + ) and ( # concrete subtypes + is_proper_subtype(current_type, proposed_type, ignore_promotions=True) + or ( # structural subtypes + is_subtype(current_type, proposed_type, ignore_promotions=True) + and ( + isinstance(proposed_type, CallableType) + or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol) ) ) - return yes_type, no_type + ): + # Expression is always of one of the types in proposed_type_ranges + return default, UninhabitedType() elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): # Expression is never of any type in proposed_type_ranges return UninhabitedType(), default diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 640fc10915d1..1326409376bd 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1086,6 +1086,21 @@ while bool(): x + 'a' [builtins fixtures/isinstance.pyi] +[case testUnreachableCode3] +# flags: --warn-unreachable --python-version 3.10 +from collections.abc import Iterable + +class A: ... + +def test(dependencies: list[A] | None) -> None: + if dependencies is None: + dependencies = [] + elif not isinstance(dependencies, Iterable): + dependencies = [dependencies] # E: Statement is unreachable + +[builtins fixtures/isinstancelist.pyi] +[typing fixtures/typing-full.pyi] + [case testUnreachableWhileTrue] def f(x: int) -> None: while True: From 143f99034605558ab92451b04269dbcb4695565a Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Sun, 20 Jul 2025 10:36:45 +0200 Subject: [PATCH 11/14] check if current_type is AnyType --- mypy/checker.py | 17 +++++--- test-data/unit/check-generic-alias.test | 32 ++++++++++++++ test-data/unit/check-isinstance.test | 15 ------- test-data/unit/check-python310.test | 57 ++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 23 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7620bab10b2b..02f6907777b4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7994,11 +7994,13 @@ def conditional_types( ) -> tuple[Type | None, Type | None]: """Takes in the current type and a proposed type of an expression. - Returns a 2-tuple: The first element is the proposed type, if the expression - can be the proposed type. The second element is the type it would hold - if it was not the proposed type, if any. UninhabitedType means unreachable. - None means no new information can be inferred. If default is set it is returned - instead.""" + Returns a 2-tuple: + The first element is the proposed type, if the expression can be the proposed type. + The second element is the type it would hold if it was not the proposed type, if any. + UninhabitedType means unreachable. + None means no new information can be inferred. + If default is set it is returned instead. + """ if proposed_type_ranges: if len(proposed_type_ranges) == 1: target = proposed_type_ranges[0].item @@ -8010,7 +8012,10 @@ def conditional_types( current_type = try_expanding_sum_type_to_union(current_type, enum_name) proposed_items = [type_range.item for type_range in proposed_type_ranges] proposed_type = make_simplified_union(proposed_items) - if isinstance(proposed_type, AnyType): + current_type = get_proper_type(current_type) + if isinstance(current_type, AnyType): + return proposed_type, current_type + elif isinstance(proposed_type, AnyType): # We don't really know much about the proposed type, so we shouldn't # attempt to narrow anything. Instead, we broaden the expr to Any to # avoid false positives diff --git a/test-data/unit/check-generic-alias.test b/test-data/unit/check-generic-alias.test index 678950a1e18b..3f088308da64 100644 --- a/test-data/unit/check-generic-alias.test +++ b/test-data/unit/check-generic-alias.test @@ -149,6 +149,38 @@ t23: collections.abc.ValuesView[str] # reveal_type(t23) # Nx Revealed type is "collections.abc.ValuesView[builtins.str]" [builtins fixtures/tuple.pyi] +[case testGenericAliasIsinstanceUnreachable] +# flags: --warn-unreachable --python-version 3.10 +from collections.abc import Iterable + +class A: ... + +def test(dependencies: list[A] | None) -> None: + if dependencies is None: + dependencies = [] + elif not isinstance(dependencies, Iterable): + dependencies = [dependencies] # E: Statement is unreachable + +[builtins fixtures/isinstancelist.pyi] +[typing fixtures/typing-full.pyi] + +[case testGenericAliasRedundantExprCompoundIfExpr] +# flags: --warn-unreachable --enable-error-code=redundant-expr --python-version 3.10 + +from typing import Any, reveal_type +from collections.abc import Iterable + +def test_example(x: Iterable[Any]) -> None: + if isinstance(x, Iterable) and not isinstance(x, str): # E: Left operand of "and" is always true + reveal_type(x) # N: Revealed type is "typing.Iterable[Any]" + +def test_counterexample(x: Any) -> None: + if isinstance(x, Iterable) and not isinstance(x, str): + reveal_type(x) # N: Revealed type is "typing.Iterable[Any]" + +[builtins fixtures/isinstancelist.pyi] +[typing fixtures/typing-full.pyi] + [case testGenericBuiltinTupleTyping] from typing import Tuple diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 1326409376bd..640fc10915d1 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1086,21 +1086,6 @@ while bool(): x + 'a' [builtins fixtures/isinstance.pyi] -[case testUnreachableCode3] -# flags: --warn-unreachable --python-version 3.10 -from collections.abc import Iterable - -class A: ... - -def test(dependencies: list[A] | None) -> None: - if dependencies is None: - dependencies = [] - elif not isinstance(dependencies, Iterable): - dependencies = [dependencies] # E: Statement is unreachable - -[builtins fixtures/isinstancelist.pyi] -[typing fixtures/typing-full.pyi] - [case testUnreachableWhileTrue] def f(x: int) -> None: while True: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index d3e14d783fac..a197cf353a25 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -11,11 +11,30 @@ match m: -- Literal Pattern -- [case testMatchLiteralPatternNarrows] +# flags: --warn-unreachable m: object match m: case 1: reveal_type(m) # N: Revealed type is "Literal[1]" + case 2: + reveal_type(m) # N: Revealed type is "Literal[2]" + case other: + reveal_type(other) # N: Revealed type is "builtins.object" + +[case testMatchLiteralPatternNarrows2] +# flags: --warn-unreachable +from typing import Any + +m: Any + +match m: + case 1: + reveal_type(m) # N: Revealed type is "Literal[1]" + case 2: + reveal_type(m) # N: Revealed type is "Literal[2]" + case other: + reveal_type(other) # N: Revealed type is "Any" [case testMatchLiteralPatternAlreadyNarrower-skip] m: bool @@ -1070,28 +1089,42 @@ match m: pass [case testMatchClassPatternCallable] -from typing import Callable +# flags: --warn-unreachable +from typing import Callable, Any class FnImpl: def __call__(self, x: object, /) -> int: ... +def test_any(x: Any) -> None: + match x: + case Callable() as fn: + reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" + case other: + reveal_type(other) # N: Revealed type is "Any" + def test_object(x: object) -> None: match x: case Callable() as fn: reveal_type(fn) # N: Revealed type is "def (*Any, **Any) -> Any" + case other: + reveal_type(other) # N: Revealed type is "builtins.object" def test_impl(x: FnImpl) -> None: match x: case Callable() as fn: reveal_type(fn) # N: Revealed type is "__main__.FnImpl" + case other: + reveal_type(other) # E: Statement is unreachable def test_callable(x: Callable[[object], int]) -> None: match x: case Callable() as fn: reveal_type(fn) # N: Revealed type is "def (builtins.object) -> builtins.int" - + case other: + reveal_type(other) # E: Statement is unreachable [case testMatchClassPatternCallbackProtocol] +# flags: --warn-unreachable from typing import Any, Callable from typing_extensions import Protocol, runtime_checkable @@ -1102,24 +1135,38 @@ class FnProto(Protocol): class FnImpl: def __call__(self, x: object, /) -> int: ... +def test_any(x: Any) -> None: + match x: + case FnProto() as fn: + reveal_type(fn) # N: Revealed type is "__main__.FnProto" + case other: + reveal_type(other) # N: Revealed type is "Any" + def test_object(x: object) -> None: match x: case FnProto() as fn: reveal_type(fn) # N: Revealed type is "__main__.FnProto" + case other: + reveal_type(other) # N: Revealed type is "builtins.object" def test_impl(x: FnImpl) -> None: match x: case FnProto() as fn: reveal_type(fn) # N: Revealed type is "__main__.FnImpl" + case other: + reveal_type(other) # E: Statement is unreachable def test_callable(x: Callable[[object], int]) -> None: match x: case FnProto() as fn: reveal_type(fn) # N: Revealed type is "def (builtins.object) -> builtins.int" + case other: + reveal_type(other) # E: Statement is unreachable [builtins fixtures/dict.pyi] [case testMatchClassPatternAnyCallableProtocol] +# flags: --warn-unreachable from typing import Any, Callable from typing_extensions import Protocol, runtime_checkable @@ -1134,16 +1181,22 @@ def test_object(x: object) -> None: match x: case AnyCallable() as fn: reveal_type(fn) # N: Revealed type is "__main__.AnyCallable" + case other: + reveal_type(other) # N: Revealed type is "builtins.object" def test_impl(x: FnImpl) -> None: match x: case AnyCallable() as fn: reveal_type(fn) # N: Revealed type is "__main__.FnImpl" + case other: + reveal_type(other) # E: Statement is unreachable def test_callable(x: Callable[[object], int]) -> None: match x: case AnyCallable() as fn: reveal_type(fn) # N: Revealed type is "def (builtins.object) -> builtins.int" + case other: + reveal_type(other) # E: Statement is unreachable [builtins fixtures/dict.pyi] From a3d469fb74447835c1177c023e525f9a71d712ce Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Sun, 27 Jul 2025 18:46:08 +0200 Subject: [PATCH 12/14] Update test-data/unit/check-python310.test Co-authored-by: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> --- test-data/unit/check-python310.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index a197cf353a25..e8e1357964c0 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1205,6 +1205,7 @@ def test_callable(x: Callable[[object], int]) -> None: from typing import Any from typing_extensions import Protocol, runtime_checkable +@runtime_checkable class Proto(Protocol): def foo(self, x: int, /) -> object: ... From e80d6587b7768f76e2ebf985085196fdf4b73fce Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Sun, 27 Jul 2025 18:47:00 +0200 Subject: [PATCH 13/14] Update mypy/checker.py Co-authored-by: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> --- mypy/checker.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 02f6907777b4..e7585b0384fa 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8022,14 +8022,16 @@ def conditional_types( return proposed_type, default elif not any( type_range.is_upper_bound for type_range in proposed_type_ranges - ) and ( # concrete subtypes + ) and ( + # concrete subtypes is_proper_subtype(current_type, proposed_type, ignore_promotions=True) - or ( # structural subtypes - is_subtype(current_type, proposed_type, ignore_promotions=True) - and ( + # structural subtypes + or ( + ( isinstance(proposed_type, CallableType) or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol) ) + and is_subtype(current_type, proposed_type, ignore_promotions=True) ) ): # Expression is always of one of the types in proposed_type_ranges From 6a1ad26a1dabb5ee74386730dc6acc193326b225 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:48:47 +0000 Subject: [PATCH 14/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index bcbf12bf3c34..fa35741c2958 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8022,9 +8022,7 @@ def conditional_types( # attempt to narrow anything. Instead, we broaden the expr to Any to # avoid false positives return proposed_type, default - elif not any( - type_range.is_upper_bound for type_range in proposed_type_ranges - ) and ( + elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and ( # concrete subtypes is_proper_subtype(current_type, proposed_type, ignore_promotions=True) # structural subtypes