Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
From 9b584b626fc18881055a3f48080fe3afcf9bb583 Mon Sep 17 00:00:00 2001
From: Christoph Tyralla <c.tyralla@bjoernsen.de>
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

34 changes: 11 additions & 23 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -4895,6 +4896,7 @@ def infer_context_dependent(
return typ

def check_return_stmt(self, s: ReturnStmt) -> None:

defn = self.scope.current_function()
if defn is not None:
if defn.is_generator:
Expand Down Expand Up @@ -4942,17 +4944,11 @@ 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)
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
Expand All @@ -4961,10 +4957,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"
Expand All @@ -4983,9 +4975,12 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
return
self.fail(message_registry.NO_RETURN_VALUE_EXPECTED, s)
else:
typ_: Type = typ
if defn.name in BINARY_MAGIC_METHODS or defn.name == "__subclasshook__":
typ_ = erase_notimplemented(typ)
self.check_subtype(
subtype_label="got",
subtype=typ,
subtype=typ_,
supertype_label="expected",
supertype=return_type,
context=s.expr,
Expand Down Expand Up @@ -5098,22 +5093,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."""

Expand Down
10 changes: 6 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -3554,7 +3555,7 @@ def visit_op_expr(self, e: OpExpr) -> Type:
else:
assert_never(use_reverse)
e.method_type = method_type
return result
return erase_notimplemented(result)
else:
raise RuntimeError(f"Unknown operator {e.op}")

Expand Down Expand Up @@ -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 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.
Expand Down Expand Up @@ -4228,15 +4229,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 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 erase_notimplemented(result), inferred

def check_boolean_op(self, e: OpExpr) -> Type:
"""Type check a boolean operation ('and' or 'or')."""
Expand Down
1 change: 1 addition & 0 deletions mypy/mro.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def calculate_mro(info: TypeInfo, obj_type: Callable[[], Instance] | None = None
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)

type_state.reset_all_subtype_caches_for(info)


Expand Down
15 changes: 15 additions & 0 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion mypy/typeshed/stdlib/builtins.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,7 @@ class property:

@final
@type_check_only
class _NotImplementedType(Any):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...wait, but you still need a patch, right? (misc/typeshed_patches directory seems to be where they live) Our typeshed updates routine (misc/sync-typeshed.py) is "clone the HEAD of current typeshed, then apply patches in order", so your change will be lost during the next sync

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a short look at it, and you are likely right. I have to admit that I am absolutely unfamiliar with the workflow and not very interested in spending time learning it. Would you like to contribute this change here (or in a separate PR - I do not know...)? Otherwise, I would simply go back to the original solution.

Copy link
Collaborator

@sterliakov sterliakov Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not deeply familiar with that either, but... Just git format-patch -1 -o misc/typeshed_patches/ 9b584b mypy/typeshed/ should do the trick (rename and edit the Subject line of the new file if you wish, commit&push) - I can open a PR with that file in your fork, but might be easier to just generate it on your end? (I didn't run the command, but the patches all look like format-patch output, and I'm moderately certain that I remember its arguments correctly)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks a lot for your help; it seems to have worked exactly as you suggested!

class _NotImplementedType:
__call__: None

NotImplemented: _NotImplementedType
Expand Down
93 changes: 93 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -6852,3 +6852,96 @@ 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]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you move this to check-overloading?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests were originally in the "Returning Any" section of check-warnings.test, but that section no longer fits. Since returning NotImplemented is usually applied in the context of operator overloading, I moved the tests to check-overloading.test. Do you know a place that fits better?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably put them in check-classes.test, but that one is already too big, so I'm fine with your decision - just a bit weird to have a set of tests without a single @overload in an overloads test file:)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. But we may add many explicit overloads to them later...

# flags: --warn-return-any
from typing import Union

class A:
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]
# flags: --warn-return-any
class A:
@classmethod
def __subclasshook__(cls, t: type[object], /) -> bool:
return NotImplemented
[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")
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]

[case testNotImplementedReturnedFromBinaryMagicMethod]
# flags: --warn-return-any
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]
15 changes: 0 additions & 15 deletions test-data/unit/check-warnings.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion test-data/unit/fixtures/notimplemented.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ class function: pass
class bool: pass
class int: pass
class str: pass
class tuple: pass
class dict: pass
class classmethod: pass
class ellipsis: pass

class _NotImplementedType(Any):
class _NotImplementedType:
__call__: NotImplemented # type: ignore
NotImplemented: _NotImplementedType

NotImplementedType = _NotImplementedType

class BaseException: pass