Skip to content

Commit 62a971c

Browse files
Emit error for "raise NotImplemented" (#17890)
Refs #5710 Adds special-case handling for raising `NotImplemented`: ```python raise NotImplemented # E: Exception must be derived from BaseException; did you mean "NotImplementedError"? ``` Per the linked issue, there's some debate as to how to best handle `NotImplemented`. This PR special-cases its behavior in `raise` statements, whereas the leaning in the issue (at the time it was opened) was towards updating its definition in typeshed and possibly special-casing its use in the relevant dunder methods. Going the typeshed/special-dunder route may still happen, but it hasn't in the six years since the issue was opened. It would be nice to at least catch errors from raising `NotImplemented` in the interim. Making this change also uncovered a regression introduced in python/typeshed#4222. Previously, `NotImplemented` was annotated as `Any` in typeshed, so returning `NotImplemented` from a non-dunder would emit an error when `--warn-return-any` was used: ```python class A: def some(self) -> bool: return NotImplemented # E: Returning Any from function declared to return "bool" ``` However, in python/typeshed#4222, the type of `NotImplemented` was updated to be a subclass of `Any`. This broke the handling of `--warn-return-any`, but it wasn't caught since the definition of `NotImplemented` in `fixtures/notimplemented.pyi` wasn't updated along with the definition in typeshed. As a result, current mypy doesn't emit an error here ([playground](https://mypy-play.net/?mypy=1.11.2&python=3.12&flags=strict%2Cwarn-return-any&gist=8a78e3eb68b0b738f73fdd326f0bfca1)), despite having a test case saying that it should. As a bandaid, this PR add special handling for `NotImplemented` in return statements to treat it like an explicit `Any`, restoring the pre- python/typeshed#4222 behavior.
1 parent a1fa1c4 commit 62a971c

File tree

3 files changed

+29
-3
lines changed

3 files changed

+29
-3
lines changed

mypy/checker.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4581,6 +4581,13 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
45814581
s.expr, return_type, allow_none_return=allow_none_func_call
45824582
)
45834583
)
4584+
# Treat NotImplemented as having type Any, consistent with its
4585+
# definition in typeshed prior to python/typeshed#4222.
4586+
if (
4587+
isinstance(typ, Instance)
4588+
and typ.type.fullname == "builtins._NotImplementedType"
4589+
):
4590+
typ = AnyType(TypeOfAny.special_form)
45844591

45854592
if defn.is_async_generator:
45864593
self.fail(message_registry.RETURN_IN_ASYNC_GENERATOR, s)
@@ -4746,6 +4753,14 @@ def type_check_raise(self, e: Expression, s: RaiseStmt, optional: bool = False)
47464753
# https://github.com/python/mypy/issues/11089
47474754
self.expr_checker.check_call(typ, [], [], e)
47484755

4756+
if isinstance(typ, Instance) and typ.type.fullname == "builtins._NotImplementedType":
4757+
self.fail(
4758+
message_registry.INVALID_EXCEPTION.with_additional_msg(
4759+
'; did you mean "NotImplementedError"?'
4760+
),
4761+
s,
4762+
)
4763+
47494764
def visit_try_stmt(self, s: TryStmt) -> None:
47504765
"""Type check a try statement."""
47514766
# Our enclosing frame will get the result if the try/except falls through.

test-data/unit/check-statements.test

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,13 @@ if object():
519519
raise BaseException from f # E: Exception must be derived from BaseException
520520
[builtins fixtures/exception.pyi]
521521

522+
[case testRaiseNotImplementedFails]
523+
if object():
524+
raise NotImplemented # E: Exception must be derived from BaseException; did you mean "NotImplementedError"?
525+
if object():
526+
raise NotImplemented() # E: NotImplemented? not callable
527+
[builtins fixtures/notimplemented.pyi]
528+
522529
[case testTryFinallyStatement]
523530
import typing
524531
try:
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# builtins stub used in NotImplemented related cases.
2-
from typing import Any, cast
3-
2+
from typing import Any
43

54
class object:
65
def __init__(self) -> None: pass
@@ -10,5 +9,10 @@ class function: pass
109
class bool: pass
1110
class int: pass
1211
class str: pass
13-
NotImplemented = cast(Any, None)
1412
class dict: pass
13+
14+
class _NotImplementedType(Any):
15+
__call__: NotImplemented # type: ignore
16+
NotImplemented: _NotImplementedType
17+
18+
class BaseException: pass

0 commit comments

Comments
 (0)