Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
48 changes: 25 additions & 23 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4894,7 +4894,23 @@ def infer_context_dependent(
self.store_types(original_type_map)
return typ

@staticmethod
def is_notimplemented(t: ProperType) -> bool:
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.

Nit: are staticmethods as fast as bare functions with mypyc? (I really don't know the answer here, just wondering)

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 have no idea. But yes, maybe a relevant point, as it could be called quite often for many code bases.

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 look. Both @staticmethod and @classmethod are very rarely used in performance-critical code. (The only static method in semanal, get_deprecated, was introduced by me...) So performance might, in fact, be an issue. Or it's just a question of style. Whatever, I turned both is_notimplemented and erase_notimplemented into normal functions and moved them to typeops.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Cool! I still don't know whether it makes any difference, but using free functions avoids this question altogether:)

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()
if defn is not None:
if defn.is_generator:
Expand Down Expand Up @@ -4942,17 +4958,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 +4971,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 +4989,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_ = self.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 +5107,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
9 changes: 5 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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')."""
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
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
90 changes: 90 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -6852,3 +6852,93 @@ 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...

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]
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]

[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]
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