Add error code "explicit-override" for strict @override mode (PEP 698) (#15512)
Add the strict mode for [PEP
698](https://peps.python.org/pep-0698/#strict-enforcement-per-project).
Closes: #14072
diff --git a/docs/source/class_basics.rst b/docs/source/class_basics.rst
index 82bbf00..73f95f1 100644
--- a/docs/source/class_basics.rst
+++ b/docs/source/class_basics.rst
@@ -210,7 +210,9 @@
In order to ensure that your code remains correct when renaming methods,
it can be helpful to explicitly mark a method as overriding a base
-method. This can be done with the ``@override`` decorator. If the base
+method. This can be done with the ``@override`` decorator. ``@override``
+can be imported from ``typing`` starting with Python 3.12 or from
+``typing_extensions`` for use with older Python versions. If the base
method is then renamed while the overriding method is not, mypy will
show an error:
@@ -233,6 +235,11 @@
def g(self, y: str) -> None: # Error: no corresponding base method found
...
+.. note::
+
+ Use :ref:`--enable-error-code explicit-override <code-explicit-override>` to require
+ that method overrides use the ``@override`` decorator. Emit an error if it is missing.
+
You can also override a statically typed method with a dynamically
typed one. This allows dynamically typed code to override methods
defined in library classes without worrying about their type
diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst
index e1d47f7..30fad07 100644
--- a/docs/source/error_code_list2.rst
+++ b/docs/source/error_code_list2.rst
@@ -442,3 +442,42 @@
# The following will not generate an error on either
# Python 3.8, or Python 3.9
42 + "testing..." # type: ignore
+
+.. _code-explicit-override:
+
+Check that ``@override`` is used when overriding a base class method [explicit-override]
+----------------------------------------------------------------------------------------
+
+If you use :option:`--enable-error-code explicit-override <mypy --enable-error-code>`
+mypy generates an error if you override a base class method without using the
+``@override`` decorator. An error will not be emitted for overrides of ``__init__``
+or ``__new__``. See `PEP 698 <https://peps.python.org/pep-0698/#strict-enforcement-per-project>`_.
+
+.. note::
+
+ Starting with Python 3.12, the ``@override`` decorator can be imported from ``typing``.
+ To use it with older Python versions, import it from ``typing_extensions`` instead.
+
+Example:
+
+.. code-block:: python
+
+ # Use "mypy --enable-error-code explicit-override ..."
+
+ from typing import override
+
+ class Parent:
+ def f(self, x: int) -> None:
+ pass
+
+ def g(self, y: int) -> None:
+ pass
+
+
+ class Child(Parent):
+ def f(self, x: int) -> None: # Error: Missing @override decorator
+ pass
+
+ @override
+ def g(self, y: int) -> None:
+ pass
diff --git a/mypy/checker.py b/mypy/checker.py
index 5ed1c79..71c9746 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -643,9 +643,14 @@
if defn.impl:
defn.impl.accept(self)
if defn.info:
- found_base_method = self.check_method_override(defn)
- if defn.is_explicit_override and found_base_method is False:
+ found_method_base_classes = self.check_method_override(defn)
+ if (
+ defn.is_explicit_override
+ and not found_method_base_classes
+ and found_method_base_classes is not None
+ ):
self.msg.no_overridable_method(defn.name, defn)
+ self.check_explicit_override_decorator(defn, found_method_base_classes, defn.impl)
self.check_inplace_operator_method(defn)
if not defn.is_property:
self.check_overlapping_overloads(defn)
@@ -972,7 +977,8 @@
# overload, the legality of the override has already
# been typechecked, and decorated methods will be
# checked when the decorator is.
- self.check_method_override(defn)
+ found_method_base_classes = self.check_method_override(defn)
+ self.check_explicit_override_decorator(defn, found_method_base_classes)
self.check_inplace_operator_method(defn)
if defn.original_def:
# Override previous definition.
@@ -1813,23 +1819,41 @@
else:
return [(defn, typ)]
- def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | None:
+ def check_explicit_override_decorator(
+ self,
+ defn: FuncDef | OverloadedFuncDef,
+ found_method_base_classes: list[TypeInfo] | None,
+ context: Context | None = None,
+ ) -> None:
+ if (
+ found_method_base_classes
+ and not defn.is_explicit_override
+ and defn.name not in ("__init__", "__new__")
+ ):
+ self.msg.explicit_override_decorator_missing(
+ defn.name, found_method_base_classes[0].fullname, context or defn
+ )
+
+ def check_method_override(
+ self, defn: FuncDef | OverloadedFuncDef | Decorator
+ ) -> list[TypeInfo] | None:
"""Check if function definition is compatible with base classes.
This may defer the method if a signature is not available in at least one base class.
Return ``None`` if that happens.
- Return ``True`` if an attribute with the method name was found in the base class.
+ Return a list of base classes which contain an attribute with the method name.
"""
# Check against definitions in base classes.
- found_base_method = False
+ found_method_base_classes: list[TypeInfo] = []
for base in defn.info.mro[1:]:
result = self.check_method_or_accessor_override_for_base(defn, base)
if result is None:
# Node was deferred, we will have another attempt later.
return None
- found_base_method |= result
- return found_base_method
+ if result:
+ found_method_base_classes.append(base)
+ return found_method_base_classes
def check_method_or_accessor_override_for_base(
self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
@@ -4739,9 +4763,14 @@
self.check_incompatible_property_override(e)
# For overloaded functions we already checked override for overload as a whole.
if e.func.info and not e.func.is_dynamic() and not e.is_overload:
- found_base_method = self.check_method_override(e)
- if e.func.is_explicit_override and found_base_method is False:
+ found_method_base_classes = self.check_method_override(e)
+ if (
+ e.func.is_explicit_override
+ and not found_method_base_classes
+ and found_method_base_classes is not None
+ ):
self.msg.no_overridable_method(e.func.name, e.func)
+ self.check_explicit_override_decorator(e.func, found_method_base_classes)
if e.func.info and e.func.name in ("__init__", "__new__"):
if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)):
diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py
index 68ae4b4..717629a 100644
--- a/mypy/errorcodes.py
+++ b/mypy/errorcodes.py
@@ -235,6 +235,12 @@
UNUSED_IGNORE: Final = ErrorCode(
"unused-ignore", "Ensure that all type ignores are used", "General", default_enabled=False
)
+EXPLICIT_OVERRIDE_REQUIRED: Final = ErrorCode(
+ "explicit-override",
+ "Require @override decorator if method is overriding a base class method",
+ "General",
+ default_enabled=False,
+)
# Syntax errors are often blocking.
diff --git a/mypy/messages.py b/mypy/messages.py
index 021ad2c..ae7fba1 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -1525,6 +1525,16 @@
context,
)
+ def explicit_override_decorator_missing(
+ self, name: str, base_name: str, context: Context
+ ) -> None:
+ self.fail(
+ f'Method "{name}" is not using @override '
+ f'but is overriding a method in class "{base_name}"',
+ context,
+ code=codes.EXPLICIT_OVERRIDE_REQUIRED,
+ )
+
def final_cant_override_writable(self, name: str, ctx: Context) -> None:
self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx)
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
index 141d18a..0de4798 100644
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -2759,8 +2759,7 @@
class F(E):
@override
def f(self, x: int) -> str: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
[case explicitOverrideStaticmethod]
# flags: --python-version 3.12
@@ -2792,8 +2791,8 @@
def f(x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
# N: This violates the Liskov substitution principle \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/callable.pyi]
+[typing fixtures/typing-override.pyi]
+[builtins fixtures/staticmethod.pyi]
[case explicitOverrideClassmethod]
# flags: --python-version 3.12
@@ -2825,8 +2824,8 @@
def f(cls, x: str) -> str: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
# N: This violates the Liskov substitution principle \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/callable.pyi]
+[typing fixtures/typing-override.pyi]
+[builtins fixtures/classmethod.pyi]
[case explicitOverrideProperty]
# flags: --python-version 3.12
@@ -2860,8 +2859,8 @@
# N: str \
# N: Subclass: \
# N: int
+[typing fixtures/typing-override.pyi]
[builtins fixtures/property.pyi]
-[typing fixtures/typing-full.pyi]
[case explicitOverrideSettableProperty]
# flags: --python-version 3.12
@@ -2898,8 +2897,8 @@
@f.setter
def f(self, value: int) -> None: pass
+[typing fixtures/typing-override.pyi]
[builtins fixtures/property.pyi]
-[typing fixtures/typing-full.pyi]
[case invalidExplicitOverride]
# flags: --python-version 3.12
@@ -2914,8 +2913,7 @@
def g() -> None:
@override # E: "override" used with a non-method
def h(b: bool) -> int: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
[case explicitOverrideSpecialMethods]
# flags: --python-version 3.12
@@ -2931,8 +2929,7 @@
class C:
@override
def __init__(self, a: int) -> None: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
[case explicitOverrideFromExtensions]
from typing_extensions import override
@@ -2943,7 +2940,6 @@
class B(A):
@override
def f2(self, x: int) -> str: pass # E: Method "f2" is marked as an override, but no base method was found with this name
-[typing fixtures/typing-full.pyi]
[builtins fixtures/tuple.pyi]
[case explicitOverrideOverloads]
@@ -2960,8 +2956,7 @@
def f2(self, x: str) -> str: pass
@override
def f2(self, x: int | str) -> str: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
[case explicitOverrideNotOnOverloadsImplementation]
# flags: --python-version 3.12
@@ -2985,8 +2980,7 @@
@overload
def f(self, y: str) -> str: pass
def f(self, y: int | str) -> str: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
[case explicitOverrideOnMultipleOverloads]
# flags: --python-version 3.12
@@ -3012,5 +3006,157 @@
def f(self, y: str) -> str: pass
@override
def f(self, y: int | str) -> str: pass
-[typing fixtures/typing-full.pyi]
+[typing fixtures/typing-override.pyi]
+
+[case explicitOverrideCyclicDependency]
+# flags: --python-version 3.12
+import b
+[file a.py]
+from typing import override
+import b
+import c
+
+class A(b.B):
+ @override # This is fine
+ @c.deco
+ def meth(self) -> int: ...
+[file b.py]
+import a
+import c
+
+class B:
+ @c.deco
+ def meth(self) -> int: ...
+[file c.py]
+from typing import TypeVar, Tuple, Callable
+T = TypeVar('T')
+def deco(f: Callable[..., T]) -> Callable[..., Tuple[T, int]]: ...
[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideMethod]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import override
+
+class A:
+ def f(self, x: int) -> str: pass
+
+class B(A):
+ @override
+ def f(self, y: int) -> str: pass
+
+class C(A):
+ def f(self, y: int) -> str: pass # E: Method "f" is not using @override but is overriding a method in class "__main__.A"
+
+class D(B):
+ def f(self, y: int) -> str: pass # E: Method "f" is not using @override but is overriding a method in class "__main__.B"
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideSpecialMethod]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import Callable, Self, TypeVar, override, overload
+
+T = TypeVar('T')
+def some_decorator(f: Callable[..., T]) -> Callable[..., T]: ...
+
+# Don't require override decorator for __init__ and __new__
+# See: https://github.com/python/typing/issues/1376
+class A:
+ def __init__(self) -> None: pass
+ def __new__(cls) -> Self: pass
+
+class B(A):
+ def __init__(self) -> None: pass
+ def __new__(cls) -> Self: pass
+
+class C(A):
+ @some_decorator
+ def __init__(self) -> None: pass
+
+ @some_decorator
+ def __new__(cls) -> Self: pass
+
+class D(A):
+ @overload
+ def __init__(self, x: int) -> None: ...
+ @overload
+ def __init__(self, x: str) -> None: ...
+ def __init__(self, x): pass
+
+ @overload
+ def __new__(cls, x: int) -> Self: pass
+ @overload
+ def __new__(cls, x: str) -> Self: pass
+ def __new__(cls, x): pass
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideProperty]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import override
+
+class A:
+ @property
+ def prop(self) -> int: pass
+
+class B(A):
+ @override
+ @property
+ def prop(self) -> int: pass
+
+class C(A):
+ @property
+ def prop(self) -> int: pass # E: Method "prop" is not using @override but is overriding a method in class "__main__.A"
+[typing fixtures/typing-override.pyi]
+[builtins fixtures/property.pyi]
+
+[case requireExplicitOverrideOverload]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import overload, override
+
+class A:
+ @overload
+ def f(self, x: int) -> str: ...
+ @overload
+ def f(self, x: str) -> str: ...
+ def f(self, x): pass
+
+class B(A):
+ @overload
+ def f(self, y: int) -> str: ...
+ @overload
+ def f(self, y: str) -> str: ...
+ @override
+ def f(self, y): pass
+
+class C(A):
+ @overload
+ @override
+ def f(self, y: int) -> str: ...
+ @overload
+ def f(self, y: str) -> str: ...
+ def f(self, y): pass
+
+class D(A):
+ @overload
+ def f(self, y: int) -> str: ...
+ @overload
+ def f(self, y: str) -> str: ...
+ def f(self, y): pass # E: Method "f" is not using @override but is overriding a method in class "__main__.A"
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideMultipleInheritance]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import override
+
+class A:
+ def f(self, x: int) -> str: pass
+class B:
+ def f(self, y: int) -> str: pass
+
+class C(A, B):
+ @override
+ def f(self, z: int) -> str: pass
+
+class D(A, B):
+ def f(self, z: int) -> str: pass # E: Method "f" is not using @override but is overriding a method in class "__main__.A"
+[typing fixtures/typing-override.pyi]
diff --git a/test-data/unit/fixtures/typing-override.pyi b/test-data/unit/fixtures/typing-override.pyi
new file mode 100644
index 0000000..606ca63
--- /dev/null
+++ b/test-data/unit/fixtures/typing-override.pyi
@@ -0,0 +1,25 @@
+TypeVar = 0
+Generic = 0
+Any = 0
+overload = 0
+Type = 0
+Literal = 0
+Optional = 0
+Self = 0
+Tuple = 0
+ClassVar = 0
+Callable = 0
+
+T = TypeVar('T')
+T_co = TypeVar('T_co', covariant=True)
+KT = TypeVar('KT')
+
+class Iterable(Generic[T_co]): pass
+class Iterator(Iterable[T_co]): pass
+class Sequence(Iterable[T_co]): pass
+class Mapping(Iterable[KT], Generic[KT, T_co]):
+ def keys(self) -> Iterable[T]: pass # Approximate return type
+ def __getitem__(self, key: T) -> T_co: pass
+
+
+def override(__arg: T) -> T: ...