Skip to content

Commit de4590e

Browse files
committed
Add strict_override_decorator option (PEP 698)
1 parent 4d94271 commit de4590e

File tree

8 files changed

+147
-1
lines changed

8 files changed

+147
-1
lines changed

docs/source/class_basics.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ show an error:
233233
def g(self, y: str) -> None: # Error: no corresponding base method found
234234
...
235235
236+
.. note::
237+
238+
Use ``--strict-override-decorator`` or
239+
:confval:`strict_override_decorator = True <strict_override_decorator>` to require
240+
methods overrides use the ``@override`` decorator. Emit an error if it is missing.
241+
236242
You can also override a statically typed method with a dynamically
237243
typed one. This allows dynamically typed code to override methods
238244
defined in library classes without worrying about their type

docs/source/config_file.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,13 @@ section of the command line docs.
714714
Prohibit equality checks, identity checks, and container checks between
715715
non-overlapping types.
716716

717+
.. confval:: strict_override_decorator
718+
719+
:type: boolean
720+
:default: False
721+
722+
Require ``override`` decorator if method is overriding a base class method.
723+
717724
.. confval:: strict
718725

719726
:type: boolean

mypy/checker.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,12 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
645645
found_base_method = self.check_method_override(defn)
646646
if defn.is_explicit_override and found_base_method is False:
647647
self.msg.no_overridable_method(defn.name, defn)
648+
elif (
649+
found_base_method
650+
and self.options.strict_override_decorator
651+
and not defn.is_explicit_override
652+
):
653+
self.msg.override_decorator_missing(defn.name, defn.impl or defn)
648654
self.check_inplace_operator_method(defn)
649655
if not defn.is_property:
650656
self.check_overlapping_overloads(defn)
@@ -971,7 +977,13 @@ def _visit_func_def(self, defn: FuncDef) -> None:
971977
# overload, the legality of the override has already
972978
# been typechecked, and decorated methods will be
973979
# checked when the decorator is.
974-
self.check_method_override(defn)
980+
found_base_method = self.check_method_override(defn)
981+
if (
982+
found_base_method
983+
and self.options.strict_override_decorator
984+
and defn.name not in ("__init__", "__new__")
985+
):
986+
self.msg.override_decorator_missing(defn.name, defn)
975987
self.check_inplace_operator_method(defn)
976988
if defn.original_def:
977989
# Override previous definition.
@@ -4742,6 +4754,12 @@ def visit_decorator(self, e: Decorator) -> None:
47424754
found_base_method = self.check_method_override(e)
47434755
if e.func.is_explicit_override and found_base_method is False:
47444756
self.msg.no_overridable_method(e.func.name, e.func)
4757+
elif (
4758+
found_base_method
4759+
and self.options.strict_override_decorator
4760+
and not e.func.is_explicit_override
4761+
):
4762+
self.msg.override_decorator_missing(e.func.name, e.func)
47454763

47464764
if e.func.info and e.func.name in ("__init__", "__new__"):
47474765
if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)):

mypy/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,14 @@ def add_invertible_flag(
825825
group=strictness_group,
826826
)
827827

828+
add_invertible_flag(
829+
"--strict-override-decorator",
830+
default=False,
831+
strict_flag=False,
832+
help="Require override decorator if method is overriding a base class method.",
833+
group=strictness_group,
834+
)
835+
828836
add_invertible_flag(
829837
"--extra-checks",
830838
default=False,

mypy/messages.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,13 @@ def no_overridable_method(self, name: str, context: Context) -> None:
15241524
context,
15251525
)
15261526

1527+
def override_decorator_missing(self, name: str, context: Context) -> None:
1528+
self.fail(
1529+
f'Method "{name}" is not marked as override '
1530+
"but is overriding a method in a base class",
1531+
context,
1532+
)
1533+
15271534
def final_cant_override_writable(self, name: str, ctx: Context) -> None:
15281535
self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx)
15291536

mypy/options.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class BuildType:
5252
"strict_concatenate",
5353
"strict_equality",
5454
"strict_optional",
55+
"strict_override_decorator",
5556
"warn_no_return",
5657
"warn_return_any",
5758
"warn_unreachable",
@@ -201,6 +202,9 @@ def __init__(self) -> None:
201202
# This makes 1 == '1', 1 in ['1'], and 1 is '1' errors.
202203
self.strict_equality = False
203204

205+
# Require override decorator. Strict mode for PEP 698.
206+
self.strict_override_decorator = False
207+
204208
# Deprecated, use extra_checks instead.
205209
self.strict_concatenate = False
206210

test-data/unit/check-functions.test

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,3 +3007,75 @@ class C(A):
30073007
def f(self, y: int | str) -> str: pass
30083008
[typing fixtures/typing-full.pyi]
30093009
[builtins fixtures/tuple.pyi]
3010+
3011+
[case requireExplicitOverrideMethod]
3012+
# flags: --strict-override-decorator --python-version 3.12
3013+
from typing import override
3014+
3015+
class A:
3016+
def f(self, x: int) -> str: pass
3017+
3018+
class B(A):
3019+
@override
3020+
def f(self, y: int) -> str: pass
3021+
3022+
class C(A):
3023+
def f(self, y: int) -> str: pass # E: Method "f" is not marked as override but is overriding a method in a base class
3024+
[typing fixtures/typing-override.pyi]
3025+
3026+
[case requireExplicitOverrideSpecialMethod]
3027+
# flags: --strict-override-decorator --python-version 3.12
3028+
from typing import Self, override
3029+
3030+
# Don't require override decorator for __init__ and __new__
3031+
# See: https://github.com/python/typing/issues/1376
3032+
class A:
3033+
def __init__(self) -> None: pass
3034+
def __new__(cls) -> Self: pass
3035+
[typing fixtures/typing-override.pyi]
3036+
3037+
[case requireExplicitOverrideProperty]
3038+
# flags: --strict-override-decorator --python-version 3.12
3039+
from typing import override
3040+
3041+
class A:
3042+
@property
3043+
def prop(self) -> int: pass
3044+
3045+
class B(A):
3046+
@override
3047+
@property
3048+
def prop(self) -> int: pass
3049+
3050+
class C(A):
3051+
@property
3052+
def prop(self) -> int: pass # E: Method "prop" is not marked as override but is overriding a method in a base class
3053+
[typing fixtures/typing-override.pyi]
3054+
[builtins fixtures/property.pyi]
3055+
3056+
[case requireExplicitOverrideOverload]
3057+
# flags: --strict-override-decorator --python-version 3.12
3058+
from typing import overload, override
3059+
3060+
class A:
3061+
@overload
3062+
def f(self, x: int) -> str: ...
3063+
@overload
3064+
def f(self, x: str) -> str: ...
3065+
def f(self, x): pass
3066+
3067+
class B(A):
3068+
@overload
3069+
def f(self, y: int) -> str: ...
3070+
@overload
3071+
def f(self, y: str) -> str: ...
3072+
@override
3073+
def f(self, y): pass
3074+
3075+
class C(A):
3076+
@overload
3077+
def f(self, y: int) -> str: ...
3078+
@overload
3079+
def f(self, y: str) -> str: ...
3080+
def f(self, y): pass # E: Method "f" is not marked as override but is overriding a method in a base class
3081+
[typing fixtures/typing-override.pyi]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
TypeVar = 0
2+
Generic = 0
3+
Any = 0
4+
overload = 0
5+
Type = 0
6+
Literal = 0
7+
Optional = 0
8+
Self = 0
9+
Tuple = 0
10+
ClassVar = 0
11+
12+
T = TypeVar('T')
13+
T_co = TypeVar('T_co', covariant=True)
14+
KT = TypeVar('KT')
15+
16+
class Iterable(Generic[T_co]): pass
17+
class Iterator(Iterable[T_co]): pass
18+
class Sequence(Iterable[T_co]): pass
19+
class Mapping(Iterable[KT], Generic[KT, T_co]):
20+
def keys(self) -> Iterable[T]: pass # Approximate return type
21+
def __getitem__(self, key: T) -> T_co: pass
22+
23+
24+
def override(__arg: T) -> T: ...

0 commit comments

Comments
 (0)