From 4143846e3923a6930727040e91493567a529fdab Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 24 Oct 2025 14:01:53 +0200 Subject: [PATCH 01/11] Do not handle `NotImplementedType` as `Any` anymore. --- mypy/checker.py | 19 ++++++++----------- mypy/mro.py | 8 ++++++-- test-data/unit/check-overloading.test | 22 ++++++++++++++++++++++ test-data/unit/check-warnings.test | 15 --------------- test-data/unit/fixtures/notimplemented.pyi | 4 ++++ 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b6a9bb3b22cd..5115f1eb3692 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4942,13 +4942,6 @@ def check_return_stmt(self, s: ReturnStmt) -> None: s.expr, return_type, allow_none_return=allow_none_func_call ) ) - # Treat NotImplemented as having type Any, consistent with its - # definition in typeshed prior to python/typeshed#4222. - if ( - isinstance(typ, Instance) - and typ.type.fullname == "builtins._NotImplementedType" - ): - typ = AnyType(TypeOfAny.special_form) if defn.is_async_generator: self.fail(message_registry.RETURN_IN_ASYNC_GENERATOR, s) @@ -4961,10 +4954,6 @@ def check_return_stmt(self, s: ReturnStmt) -> None: self.options.warn_return_any and not self.current_node_deferred and not is_proper_subtype(AnyType(TypeOfAny.special_form), return_type) - and not ( - defn.name in BINARY_MAGIC_METHODS - and is_literal_not_implemented(s.expr) - ) and not ( isinstance(return_type, Instance) and return_type.type.fullname == "builtins.object" @@ -4982,6 +4971,14 @@ def check_return_stmt(self, s: ReturnStmt) -> None: if is_lambda or isinstance(typ, NoneType): return self.fail(message_registry.NO_RETURN_VALUE_EXPECTED, s) + elif ( + isinstance(typ, Instance) + and typ.type.fullname == "builtins._NotImplementedType" + and ( + (defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__") + ) + ): + return else: self.check_subtype( subtype_label="got", diff --git a/mypy/mro.py b/mypy/mro.py index f34f3fa0c46d..33679d0dbff5 100644 --- a/mypy/mro.py +++ b/mypy/mro.py @@ -15,8 +15,12 @@ def calculate_mro(info: TypeInfo, obj_type: Callable[[], Instance] | None = None mro = linearize_hierarchy(info, obj_type) assert mro, f"Could not produce a MRO at all for {info}" info.mro = mro - # The property of falling back to Any is inherited. - info.fallback_to_any = any(baseinfo.fallback_to_any for baseinfo in info.mro) + # The property of falling back to Any is (usually) inherited. + if info.fullname == "builtins._NotImplementedType": + info.fallback_to_any = False + else: + info.fallback_to_any = any(baseinfo.fallback_to_any for baseinfo in info.mro) + type_state.reset_all_subtype_caches_for(info) diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index be55a182b87b..06e4d36ac479 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6852,3 +6852,25 @@ if isinstance(headers, dict): reveal_type(headers) # N: Revealed type is "Union[__main__.Headers, typing.Iterable[tuple[builtins.bytes, builtins.bytes]]]" [builtins fixtures/isinstancelist.pyi] + +[case testReturnNotImplementedInBinaryMagicMethods] +class A: + def __eq__(self, other: object) -> bool: return NotImplemented +[builtins fixtures/notimplemented.pyi] + +[case testReturnNotImplementedABCSubclassHookMethod] +class A: + @classmethod + def __subclasshook__(cls, t: type[object], /) -> bool: + return NotImplemented +[builtins fixtures/notimplemented.pyi] + +[case testReturnNotImplementedInNormalMethods] +from typing import Union +class A: + def f(self) -> bool: return NotImplemented # E: Incompatible return value type (got "_NotImplementedType", expected "bool") + def g(self) -> NotImplementedType: return True # E: Incompatible return value type (got "bool", expected "_NotImplementedType") + def h(self) -> NotImplementedType: return NotImplemented + def i(self) -> Union[bool, NotImplementedType]: return NotImplemented + def j(self) -> Union[bool, NotImplementedType]: return True +[builtins fixtures/notimplemented.pyi] diff --git a/test-data/unit/check-warnings.test b/test-data/unit/check-warnings.test index a2d201fa301d..69b485d62344 100644 --- a/test-data/unit/check-warnings.test +++ b/test-data/unit/check-warnings.test @@ -178,21 +178,6 @@ def f() -> int: return g() [out] main:4: error: Returning Any from function declared to return "int" -[case testReturnAnyForNotImplementedInBinaryMagicMethods] -# flags: --warn-return-any -class A: - def __eq__(self, other: object) -> bool: return NotImplemented -[builtins fixtures/notimplemented.pyi] -[out] - -[case testReturnAnyForNotImplementedInNormalMethods] -# flags: --warn-return-any -class A: - def some(self) -> bool: return NotImplemented -[builtins fixtures/notimplemented.pyi] -[out] -main:3: error: Returning Any from function declared to return "bool" - [case testReturnAnyFromTypedFunctionWithSpecificFormatting] # flags: --warn-return-any from typing import Any, Tuple diff --git a/test-data/unit/fixtures/notimplemented.pyi b/test-data/unit/fixtures/notimplemented.pyi index 92edf84a7fd1..a738a0864ba7 100644 --- a/test-data/unit/fixtures/notimplemented.pyi +++ b/test-data/unit/fixtures/notimplemented.pyi @@ -9,10 +9,14 @@ class function: pass class bool: pass class int: pass class str: pass +class tuple: pass class dict: pass +class classmethod: pass class _NotImplementedType(Any): __call__: NotImplemented # type: ignore NotImplemented: _NotImplementedType +NotImplementedType = _NotImplementedType + class BaseException: pass From 39d56bf84003edfa20da1a25e6c86727541051f0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:06:09 +0000 Subject: [PATCH 02/11] [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 5115f1eb3692..fd52f3b615d1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4974,9 +4974,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None: elif ( isinstance(typ, Instance) and typ.type.fullname == "builtins._NotImplementedType" - and ( - (defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__") - ) + and (defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__") ): return else: From 9bb029f31ccc87334399ba3fa4e1ede46edff877 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 24 Oct 2025 21:43:22 +0200 Subject: [PATCH 03/11] support unions --- mypy/checker.py | 23 ++++++++++++++--------- test-data/unit/check-overloading.test | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5115f1eb3692..d254ab32e154 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4895,6 +4895,10 @@ def infer_context_dependent( return typ def check_return_stmt(self, s: ReturnStmt) -> None: + + def is_notimplemented(t: object) -> bool: + return isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType" + defn = self.scope.current_function() if defn is not None: if defn.is_generator: @@ -4946,6 +4950,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None: if defn.is_async_generator: self.fail(message_registry.RETURN_IN_ASYNC_GENERATOR, s) return + # Returning a value of type Any is always fine. if isinstance(typ, AnyType): # (Unless you asked to be warned in that case, and the @@ -4971,18 +4976,18 @@ def check_return_stmt(self, s: ReturnStmt) -> None: if is_lambda or isinstance(typ, NoneType): return self.fail(message_registry.NO_RETURN_VALUE_EXPECTED, s) - elif ( - isinstance(typ, Instance) - and typ.type.fullname == "builtins._NotImplementedType" - and ( - (defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__") - ) - ): - return else: + typ_: Type = typ + if defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__": + if is_notimplemented(typ): + return + if isinstance(typ, UnionType): + typ_ = UnionType.make_union( + [i for i in typ.items if not is_notimplemented(i)] + ) self.check_subtype( subtype_label="got", - subtype=typ, + subtype=typ_, supertype_label="expected", supertype=return_type, context=s.expr, diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 06e4d36ac479..5bf6fba97deb 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6854,8 +6854,21 @@ reveal_type(headers) # N: Revealed type is "Union[__main__.Headers, typing.Iter [builtins fixtures/isinstancelist.pyi] [case testReturnNotImplementedInBinaryMagicMethods] +from typing import Union class A: - def __eq__(self, other: object) -> bool: return NotImplemented + def __add__(self, other: object) -> int: + return NotImplemented + def __radd__(self, other: object) -> Union[int, NotImplementedType]: + return NotImplemented + def __sub__(self, other: object) -> Union[int, NotImplementedType]: + return 1 + def __isub__(self, other: object) -> int: + x: Union[int, NotImplementedType] + return x + def __mul__(self, other: object) -> Union[int, NotImplementedType]: + x: Union[int, NotImplementedType] + return x + [builtins fixtures/notimplemented.pyi] [case testReturnNotImplementedABCSubclassHookMethod] From df6418dd43adb89be8e7026d8a478452522f3b8e Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 24 Oct 2025 22:06:30 +0200 Subject: [PATCH 04/11] fix extended raise error message --- mypy/checker.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d254ab32e154..f7f97ac163a2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5100,22 +5100,15 @@ def type_check_raise(self, e: Expression, s: RaiseStmt, optional: bool = False) # where we allow `raise e from None`. expected_type_items.append(NoneType()) - self.check_subtype( - typ, UnionType.make_union(expected_type_items), s, message_registry.INVALID_EXCEPTION - ) + message = message_registry.INVALID_EXCEPTION + if isinstance(typ, Instance) and typ.type.fullname == "builtins._NotImplementedType": + message = message.with_additional_msg('; did you mean "NotImplementedError"?') + self.check_subtype(typ, UnionType.make_union(expected_type_items), s, message) if isinstance(typ, FunctionLike): # https://github.com/python/mypy/issues/11089 self.expr_checker.check_call(typ, [], [], e) - if isinstance(typ, Instance) and typ.type.fullname == "builtins._NotImplementedType": - self.fail( - message_registry.INVALID_EXCEPTION.with_additional_msg( - '; did you mean "NotImplementedError"?' - ), - s, - ) - def visit_try_stmt(self, s: TryStmt) -> None: """Type check a try statement.""" From 9b584b626fc18881055a3f48080fe3afcf9bb583 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 25 Oct 2025 09:39:03 +0200 Subject: [PATCH 05/11] modify typeshed instead of hacking `calculate_mro` --- mypy/mro.py | 7 ++----- mypy/typeshed/stdlib/builtins.pyi | 2 +- test-data/unit/fixtures/notimplemented.pyi | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/mypy/mro.py b/mypy/mro.py index 33679d0dbff5..d5d448a73b31 100644 --- a/mypy/mro.py +++ b/mypy/mro.py @@ -15,11 +15,8 @@ def calculate_mro(info: TypeInfo, obj_type: Callable[[], Instance] | None = None mro = linearize_hierarchy(info, obj_type) assert mro, f"Could not produce a MRO at all for {info}" info.mro = mro - # The property of falling back to Any is (usually) inherited. - if info.fullname == "builtins._NotImplementedType": - info.fallback_to_any = False - else: - info.fallback_to_any = any(baseinfo.fallback_to_any for baseinfo in info.mro) + # The property of falling back to Any is inherited. + info.fallback_to_any = any(baseinfo.fallback_to_any for baseinfo in info.mro) type_state.reset_all_subtype_caches_for(info) diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi index ddf81db181bf..933a066404cc 100644 --- a/mypy/typeshed/stdlib/builtins.pyi +++ b/mypy/typeshed/stdlib/builtins.pyi @@ -1269,7 +1269,7 @@ class property: @final @type_check_only -class _NotImplementedType(Any): +class _NotImplementedType: __call__: None NotImplemented: _NotImplementedType diff --git a/test-data/unit/fixtures/notimplemented.pyi b/test-data/unit/fixtures/notimplemented.pyi index a738a0864ba7..72caa53a6ca3 100644 --- a/test-data/unit/fixtures/notimplemented.pyi +++ b/test-data/unit/fixtures/notimplemented.pyi @@ -13,7 +13,7 @@ class tuple: pass class dict: pass class classmethod: pass -class _NotImplementedType(Any): +class _NotImplementedType: __call__: NotImplemented # type: ignore NotImplemented: _NotImplementedType From 0aa07ea21c86cb721f2cd611a47946c0b5a0079a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 25 Oct 2025 17:47:50 +0200 Subject: [PATCH 06/11] erase `NotImplementedType` from the return type of normally applied binary methods ("+", "and", etc.). --- mypy/checker.py | 22 +++++---- mypy/checkexpr.py | 9 ++-- test-data/unit/check-overloading.test | 56 ++++++++++++++++++++++ test-data/unit/fixtures/notimplemented.pyi | 1 + 4 files changed, 75 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f7f97ac163a2..967c619a4283 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4894,10 +4894,19 @@ def infer_context_dependent( self.store_types(original_type_map) return typ - def check_return_stmt(self, s: ReturnStmt) -> None: + @staticmethod + def is_notimplemented(t: ProperType) -> Type: + return (isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType") + + @classmethod + def erase_notimplemented(cls, t: ProperType) -> Type: + if cls.is_notimplemented(t): + return AnyType(TypeOfAny.special_form) + if isinstance(t, UnionType): + return UnionType.make_union([i for i in t.items if not cls.is_notimplemented(i)]) + return t - def is_notimplemented(t: object) -> bool: - return isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType" + def check_return_stmt(self, s: ReturnStmt) -> None: defn = self.scope.current_function() if defn is not None: @@ -4979,12 +4988,7 @@ def is_notimplemented(t: object) -> bool: else: typ_: Type = typ if defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__": - if is_notimplemented(typ): - return - if isinstance(typ, UnionType): - typ_ = UnionType.make_union( - [i for i in typ.items if not is_notimplemented(i)] - ) + typ_ = self.erase_notimplemented(typ) self.check_subtype( subtype_label="got", subtype=typ_, diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3eb54579a050..7a8586710e87 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3554,7 +3554,7 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type - return result + return self.chk.erase_notimplemented(result) else: raise RuntimeError(f"Unknown operator {e.op}") @@ -3705,7 +3705,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: result = join.join_types(result, sub_result) assert result is not None - return result + return self.chk.erase_notimplemented(result) def find_partial_type_ref_fast_path(self, expr: Expression) -> Type | None: """If expression has a partial generic type, return it without additional checks. @@ -4228,15 +4228,16 @@ def check_op( # callable types. results_final = make_simplified_union(all_results) inferred_final = self.combine_function_signatures(get_proper_types(all_inferred)) - return results_final, inferred_final + return self.chk.erase_notimplemented(results_final), inferred_final else: - return self.check_method_call_by_name( + result, inferred = self.check_method_call_by_name( method=method, base_type=base_type, args=[arg], arg_kinds=[ARG_POS], context=context, ) + return self.chk.erase_notimplemented(result), inferred def check_boolean_op(self, e: OpExpr) -> Type: """Type check a boolean operation ('and' or 'or').""" diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 5bf6fba97deb..3d7d21cf29c1 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6887,3 +6887,59 @@ class A: def i(self) -> Union[bool, NotImplementedType]: return NotImplemented def j(self) -> Union[bool, NotImplementedType]: return True [builtins fixtures/notimplemented.pyi] + +[case testNotImplementedReturnedFromBinaryMagicMethod] +# flags: --warn-unreachable +from typing import Union + +class A: + def __add__(self, x: A) -> Union[int, NotImplementedType]: ... + def __sub__(self, x: A) -> NotImplementedType: ... + def __imul__(self, x: A) -> Union[A, NotImplementedType]: ... + def __itruediv__(self, x: A) -> Union[A, NotImplementedType]: ... + def __ifloordiv__(self, x: A) -> Union[int, NotImplementedType]: ... + def __eq__(self, x: object) -> Union[bool, NotImplementedType]: ... + def __le__(self, x: int) -> Union[bool, NotImplementedType]: ... + def __lt__(self, x: int) -> NotImplementedType: ... + def __and__(self, x: object) -> NotImplementedType: ... +class B(A): + def __radd__(self, x: A) -> Union[int, NotImplementedType]: ... + def __rsub__(self, x: A) -> NotImplementedType: ... + def __itruediv__(self, x: A) -> Union[A, NotImplementedType]: ... + def __ror__(self, x: object) -> NotImplementedType: ... + +a: A +b: B + +reveal_type(a.__add__(a)) # N: Revealed type is "Union[builtins.int, builtins._NotImplementedType]" +reveal_type(a.__sub__(a)) # N: Revealed type is "builtins._NotImplementedType" +reveal_type(a.__imul__(a)) # N: Revealed type is "Union[__main__.A, builtins._NotImplementedType]" +reveal_type(a.__eq__(a)) # N: Revealed type is "Union[builtins.bool, builtins._NotImplementedType]" +reveal_type(a.__le__(1)) # N: Revealed type is "Union[builtins.bool, builtins._NotImplementedType]" + +reveal_type(a + a) # N: Revealed type is "builtins.int" +reveal_type(a - a) # N: Revealed type is "Any" +reveal_type(a + b) # N: Revealed type is "builtins.int" +reveal_type(a - b) # N: Revealed type is "Any" +def f1(a: A) -> None: + a += a # E: Incompatible types in assignment (expression has type "int", variable has type "A") +def f2(a: A) -> None: + a -= a + reveal_type(a) # N: Revealed type is "__main__.A" +def f3(a: A) -> None: + a *= a + reveal_type(a) # N: Revealed type is "__main__.A" +def f4(a: A) -> None: + a /= a + reveal_type(a) # N: Revealed type is "__main__.A" +def f5(a: A) -> None: + a //= a # E: Result type of // incompatible in assignment +reveal_type(a == a) # N: Revealed type is "builtins.bool" +reveal_type(a == 1) # N: Revealed type is "builtins.bool" +reveal_type(a <= 1) # N: Revealed type is "builtins.bool" +reveal_type(a < 1) # N: Revealed type is "Any" +reveal_type(a and int()) # N: Revealed type is "Union[__main__.A, builtins.int]" +reveal_type(int() or a) # N: Revealed type is "Union[builtins.int, __main__.A]" + +[builtins fixtures/notimplemented.pyi] + diff --git a/test-data/unit/fixtures/notimplemented.pyi b/test-data/unit/fixtures/notimplemented.pyi index 72caa53a6ca3..6d509abbb2c4 100644 --- a/test-data/unit/fixtures/notimplemented.pyi +++ b/test-data/unit/fixtures/notimplemented.pyi @@ -12,6 +12,7 @@ class str: pass class tuple: pass class dict: pass class classmethod: pass +class ellipsis: pass class _NotImplementedType: __call__: NotImplemented # type: ignore From a9c6318b2f1c00a334c4fbc4b8adb17b7dcce7e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:49:26 +0000 Subject: [PATCH 07/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 2 +- test-data/unit/check-overloading.test | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 967c619a4283..64ca9c410a00 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4896,7 +4896,7 @@ def infer_context_dependent( @staticmethod def is_notimplemented(t: ProperType) -> Type: - return (isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType") + return isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType" @classmethod def erase_notimplemented(cls, t: ProperType) -> Type: diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 3d7d21cf29c1..4ea5cb5a145e 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6942,4 +6942,3 @@ reveal_type(a and int()) # N: Revealed type is "Union[__main__.A, builtins.int] reveal_type(int() or a) # N: Revealed type is "Union[builtins.int, __main__.A]" [builtins fixtures/notimplemented.pyi] - From 0fc0efcc906996636d13c1b243d56f5fd2cca7d7 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 25 Oct 2025 18:08:40 +0200 Subject: [PATCH 08/11] fix self check --- mypy/checker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 64ca9c410a00..71f0b5eaffa1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4895,15 +4895,18 @@ def infer_context_dependent( return typ @staticmethod - def is_notimplemented(t: ProperType) -> Type: + def is_notimplemented(t: ProperType) -> bool: return isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType" @classmethod - def erase_notimplemented(cls, t: ProperType) -> Type: + def erase_notimplemented(cls, t: Type) -> Type: + t = get_proper_type(t) if cls.is_notimplemented(t): return AnyType(TypeOfAny.special_form) if isinstance(t, UnionType): - return UnionType.make_union([i for i in t.items if not cls.is_notimplemented(i)]) + return UnionType.make_union( + [i for i in t.items if not cls.is_notimplemented(get_proper_type(i))] + ) return t def check_return_stmt(self, s: ReturnStmt) -> None: From c04458c2beaa9fa9fb9706dce6e23c8121d44867 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 25 Oct 2025 22:16:36 +0200 Subject: [PATCH 09/11] add typeshed patch --- ...-subclassing_NotImplemented-from-Any.patch | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 misc/typeshed_patches/0001-Stop-subclassing_NotImplemented-from-Any.patch diff --git a/misc/typeshed_patches/0001-Stop-subclassing_NotImplemented-from-Any.patch b/misc/typeshed_patches/0001-Stop-subclassing_NotImplemented-from-Any.patch new file mode 100644 index 000000000000..ee6f082502a4 --- /dev/null +++ b/misc/typeshed_patches/0001-Stop-subclassing_NotImplemented-from-Any.patch @@ -0,0 +1,25 @@ +From 9b584b626fc18881055a3f48080fe3afcf9bb583 Mon Sep 17 00:00:00 2001 +From: Christoph Tyralla +Date: Sat, 25 Oct 2025 09:39:03 +0200 +Subject: [PATCH] modify typeshed instead of hacking `calculate_mro` + +--- + mypy/typeshed/stdlib/builtins.pyi | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi +index ddf81db18..933a06640 100644 +--- a/mypy/typeshed/stdlib/builtins.pyi ++++ b/mypy/typeshed/stdlib/builtins.pyi +@@ -1269,7 +1269,7 @@ class property: + + @final + @type_check_only +-class _NotImplementedType(Any): ++class _NotImplementedType: + __call__: None + + NotImplemented: _NotImplementedType +-- +2.45.1.windows.1 + From d3957d0dc09da164be942a1dc1ae847fd2d04cea Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 26 Oct 2025 00:20:24 +0200 Subject: [PATCH 10/11] turn the `is_notimplemented` and `erase_notimplemented` into normal functions --- mypy/checker.py | 18 ++---------------- mypy/checkexpr.py | 9 +++++---- mypy/typeops.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 71f0b5eaffa1..1a324e02ec73 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -178,6 +178,7 @@ coerce_to_literal, custom_special_method, erase_def_to_union_or_bound, + erase_notimplemented, erase_to_bound, erase_to_union_or_bound, false_only, @@ -4894,21 +4895,6 @@ def infer_context_dependent( self.store_types(original_type_map) return typ - @staticmethod - def is_notimplemented(t: ProperType) -> bool: - return isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType" - - @classmethod - def erase_notimplemented(cls, t: Type) -> Type: - t = get_proper_type(t) - if cls.is_notimplemented(t): - return AnyType(TypeOfAny.special_form) - if isinstance(t, UnionType): - return UnionType.make_union( - [i for i in t.items if not cls.is_notimplemented(get_proper_type(i))] - ) - return t - def check_return_stmt(self, s: ReturnStmt) -> None: defn = self.scope.current_function() @@ -4991,7 +4977,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None: else: typ_: Type = typ if defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__": - typ_ = self.erase_notimplemented(typ) + typ_ = erase_notimplemented(typ) self.check_subtype( subtype_label="got", subtype=typ_, diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7a8586710e87..242c1ed8cb46 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -132,6 +132,7 @@ from mypy.typeops import ( callable_type, custom_special_method, + erase_notimplemented, erase_to_union_or_bound, false_only, fixup_partial_type, @@ -3554,7 +3555,7 @@ def visit_op_expr(self, e: OpExpr) -> Type: else: assert_never(use_reverse) e.method_type = method_type - return self.chk.erase_notimplemented(result) + return erase_notimplemented(result) else: raise RuntimeError(f"Unknown operator {e.op}") @@ -3705,7 +3706,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: result = join.join_types(result, sub_result) assert result is not None - return self.chk.erase_notimplemented(result) + return erase_notimplemented(result) def find_partial_type_ref_fast_path(self, expr: Expression) -> Type | None: """If expression has a partial generic type, return it without additional checks. @@ -4228,7 +4229,7 @@ def check_op( # callable types. results_final = make_simplified_union(all_results) inferred_final = self.combine_function_signatures(get_proper_types(all_inferred)) - return self.chk.erase_notimplemented(results_final), inferred_final + return erase_notimplemented(results_final), inferred_final else: result, inferred = self.check_method_call_by_name( method=method, @@ -4237,7 +4238,7 @@ def check_op( arg_kinds=[ARG_POS], context=context, ) - return self.chk.erase_notimplemented(result), inferred + return erase_notimplemented(result), inferred def check_boolean_op(self, e: OpExpr) -> Type: """Type check a boolean operation ('and' or 'or').""" diff --git a/mypy/typeops.py b/mypy/typeops.py index 341c96c08931..c25f6e5b1d75 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -999,6 +999,21 @@ def is_singleton_type(typ: Type) -> bool: return typ.is_singleton_type() +def is_notimplemented(t: ProperType) -> bool: + return isinstance(t, Instance) and t.type.fullname == "builtins._NotImplementedType" + + +def erase_notimplemented(t: Type) -> Type: + t = get_proper_type(t) + if is_notimplemented(t): + return AnyType(TypeOfAny.special_form) + if isinstance(t, UnionType): + return UnionType.make_union( + [i for i in t.items if not is_notimplemented(get_proper_type(i))] + ) + return t + + def try_expanding_sum_type_to_union(typ: Type, target_fullname: str) -> Type: """Attempts to recursively expand any enum Instances with the given target_fullname into a Union of all of its component LiteralTypes. From aed1acdcbf6833fcf222088c9fa787be982079e9 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 26 Oct 2025 08:26:41 +0100 Subject: [PATCH 11/11] add `--warn-return-any` flags to the new test cases - just in case --- test-data/unit/check-overloading.test | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 4ea5cb5a145e..1fe9c689f6e0 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6854,7 +6854,9 @@ reveal_type(headers) # N: Revealed type is "Union[__main__.Headers, typing.Iter [builtins fixtures/isinstancelist.pyi] [case testReturnNotImplementedInBinaryMagicMethods] +# flags: --warn-return-any from typing import Union + class A: def __add__(self, other: object) -> int: return NotImplemented @@ -6868,10 +6870,10 @@ class A: def __mul__(self, other: object) -> Union[int, NotImplementedType]: x: Union[int, NotImplementedType] return x - [builtins fixtures/notimplemented.pyi] [case testReturnNotImplementedABCSubclassHookMethod] +# flags: --warn-return-any class A: @classmethod def __subclasshook__(cls, t: type[object], /) -> bool: @@ -6879,7 +6881,9 @@ class A: [builtins fixtures/notimplemented.pyi] [case testReturnNotImplementedInNormalMethods] +# flags: --warn-return-any from typing import Union + class A: def f(self) -> bool: return NotImplemented # E: Incompatible return value type (got "_NotImplementedType", expected "bool") def g(self) -> NotImplementedType: return True # E: Incompatible return value type (got "bool", expected "_NotImplementedType") @@ -6889,7 +6893,7 @@ class A: [builtins fixtures/notimplemented.pyi] [case testNotImplementedReturnedFromBinaryMagicMethod] -# flags: --warn-unreachable +# flags: --warn-return-any from typing import Union class A: @@ -6940,5 +6944,4 @@ reveal_type(a <= 1) # N: Revealed type is "builtins.bool" reveal_type(a < 1) # N: Revealed type is "Any" reveal_type(a and int()) # N: Revealed type is "Union[__main__.A, builtins.int]" reveal_type(int() or a) # N: Revealed type is "Union[builtins.int, __main__.A]" - [builtins fixtures/notimplemented.pyi]