Repository: python/mypy

Issue:
Provide more context about why incompatible with supertype is an error
Users often don't realize that overriding a method with an incompatible signature is unsafe from a type checking perspective; see #1237 for an example. We could generate some more descriptive notes with the error message, perhaps with a link to mypy documentation what contains a detailed discussion of Liskov Substitution Principle. We could also discuss how to work around the issue.

#5704 and #5025 are related issues.


Discussion:
@JukkaL I would be happy to take a stab at this if I get a couple extra cycles soon, if were a little more clear exactly what we want here. What should the notes be exactly? Or should I just wing it?
Hey @ckarnell are you still working on it?
Here's a proposal for the notes that could be added:

```
prog.py:7: error: Argument 1 of "f" is incompatible with supertype "C"; supertype defines the argument type as "Optional[int]"
prog.py:7: note: This violates the Liskov substitution principle
prog.py:7: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
```
Hi, just want to drop a note that I haven't gotten a chance to work on this.
Hi, if no one's working on it, maybe I can?
And jump-starting to it, can someone let me know which file should define these new error/note messages? Is it one of the functions in `mypy/messages.py` that needs to be edited? 
Yeah, feel free to take a shot. Probably the messages are in mypy/messages.py, yeah. It is possible that you'll need to gather more information from the call sites (probably in `checker.py`) to produce a good message.

Resolving patch:
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -772,6 +772,14 @@ def argument_incompatible_with_supertype(
                   .format(arg_num, name, target, arg_type_in_supertype_f),
                   context,
                   code=codes.OVERRIDE)
+        self.note(
+            'This violates the Liskov substitution principle',
+            context,
+            code=codes.OVERRIDE)
+        self.note(
+            'See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides',
+            context,
+            code=codes.OVERRIDE)
 
         if name == "__eq__" and type_name:
             multiline_msg = self.comparison_method_example_msg(class_name=type_name)


Test patch:
diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test
--- a/test-data/unit/check-abstract.test
+++ b/test-data/unit/check-abstract.test
@@ -311,7 +311,9 @@ class A(metaclass=ABCMeta):
     def g(self, x: int) -> int: pass
 class B(A):
     def f(self, x: str) -> int: \
-            # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int"
+            # 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
         pass
     def g(self, x: int) -> int: pass
 [out]
@@ -327,7 +329,9 @@ class J(metaclass=ABCMeta):
     def g(self, x: str) -> str: pass
 class A(I, J):
     def f(self, x: str) -> int: pass \
-        # E: Argument 1 of "f" is incompatible with supertype "I"; supertype defines the argument type as "int"
+        # E: Argument 1 of "f" is incompatible with supertype "I"; 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
     def g(self, x: str) -> int: pass \
         # E: Return type "int" of "g" incompatible with return type "str" in supertype "J"
     def h(self) -> int: pass # Not related to any base class
@@ -342,7 +346,9 @@ class J(metaclass=ABCMeta):
 class I(J): pass
 class A(I):
     def f(self, x: str) -> int: pass \
-        # E: Argument 1 of "f" is incompatible with supertype "J"; supertype defines the argument type as "int"
+        # E: Argument 1 of "f" is incompatible with supertype "J"; 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
 [out]
 
 [case testInvalidOverridingAbstractMethod]
@@ -353,7 +359,9 @@ class J(metaclass=ABCMeta):
     def f(self, x: 'J') -> None: pass
 class I(J):
     @abstractmethod
-    def f(self, x: 'I') -> None: pass # E: Argument 1 of "f" is incompatible with supertype "J"; supertype defines the argument type as "J"
+    def f(self, x: 'I') -> None: pass # E: Argument 1 of "f" is incompatible with supertype "J"; supertype defines the argument type as "J" \
+                                      # N: This violates the Liskov substitution principle \
+                                      # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 [out]
 
 [case testAbstractClassCoAndContraVariance]
@@ -375,6 +383,8 @@ class A(I):
         pass
 [out]
 main:11: error: Argument 1 of "h" is incompatible with supertype "I"; supertype defines the argument type as "I"
+main:11: note: This violates the Liskov substitution principle
+main:11: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 main:11: error: Return type "I" of "h" incompatible with return type "A" in supertype "I"
 
 
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -282,6 +282,8 @@ class B(A):
   def h(self, x: A, y: 'B') -> object: pass  # Fail
 [out]
 main:7: error: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "A"
+main:7: note: This violates the Liskov substitution principle
+main:7: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 main:9: error: Return type "object" of "h" incompatible with return type "A" in supertype "A"
 
 [case testEqMethodsOverridingWithNonObjects]
@@ -290,6 +292,8 @@ class A:
 [builtins fixtures/attr.pyi]
 [out]
 main:2: error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object"
+main:2: note: This violates the Liskov substitution principle
+main:2: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 main:2: note: It is recommended for "__eq__" to work with arbitrary objects, for example:
 main:2: note:     def __eq__(self, other: object) -> bool:
 main:2: note:         if not isinstance(other, A):
@@ -318,6 +322,8 @@ class C(B): # with gap in implementations
         pass
 [out]
 main:6: error: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "B"
+main:6: note: This violates the Liskov substitution principle
+main:6: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 
 [case testMethodOverridingAcrossDeepInheritanceHierarchy2]
 import typing
@@ -459,7 +465,9 @@ class B(A):
     @staticmethod
     def f(x: int, y: str) -> None: pass
     @staticmethod
-    def g(x: str, y: str) -> None: pass # E: Argument 1 of "g" is incompatible with supertype "A"; supertype defines the argument type as "int"
+    def g(x: str, y: str) -> None: pass # E: Argument 1 of "g" 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
 [builtins fixtures/classmethod.pyi]
 
 [case testOverrideClassMethodWithClassMethod]
@@ -473,7 +481,9 @@ class B(A):
     @classmethod
     def f(cls, x: int, y: str) -> None: pass
     @classmethod
-    def g(cls, x: str, y: str) -> None: pass # E: Argument 1 of "g" is incompatible with supertype "A"; supertype defines the argument type as "int"
+    def g(cls, x: str, y: str) -> None: pass # E: Argument 1 of "g" 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
 [builtins fixtures/classmethod.pyi]
 
 [case testOverrideClassMethodWithStaticMethod]
@@ -489,7 +499,9 @@ class B(A):
     @staticmethod
     def f(x: int) -> None: pass
     @staticmethod
-    def g(x: str) -> int: pass # E: Argument 1 of "g" is incompatible with supertype "A"; supertype defines the argument type as "int"
+    def g(x: str) -> int: pass # E: Argument 1 of "g" 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
     @staticmethod
     def h() -> int: pass
 [builtins fixtures/classmethod.pyi]
@@ -507,7 +519,9 @@ class B(A):
     @classmethod
     def f(cls, x: int) -> None: pass
     @classmethod
-    def g(cls, x: int) -> int: pass # E: Argument 1 of "g" is incompatible with supertype "A"; supertype defines the argument type as "str"
+    def g(cls, x: int) -> int: pass # E: Argument 1 of "g" is incompatible with supertype "A"; supertype defines the argument type as "str" \
+                                    # N: This violates the Liskov substitution principle \
+                                    # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
     @classmethod
     def h(cls) -> int: pass
 [builtins fixtures/classmethod.pyi]
@@ -2478,6 +2492,8 @@ class D(A):
 [out]
 main:6: error: Return type "A" of "__iadd__" incompatible with return type "B" in "__add__" of supertype "A"
 main:8: error: Argument 1 of "__iadd__" is incompatible with "__add__" of supertype "A"; supertype defines the argument type as "A"
+main:8: note: This violates the Liskov substitution principle
+main:8: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 main:8: error: Signatures of "__iadd__" and "__add__" are incompatible
 
 [case testGetattribute]
diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test
--- a/test-data/unit/check-columns.test
+++ b/test-data/unit/check-columns.test
@@ -262,7 +262,9 @@ if int():
 class A:
     def f(self, x: int) -> None: pass
 class B(A):
-    def f(self, x: str) -> None: pass # E:5: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int"
+    def f(self, x: str) -> None: pass # E:5: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
+                                      # N:5: This violates the Liskov substitution principle \
+                                      # N:5: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 class C(A):
     def f(self, x: int) -> int: pass # E:5: Return type "int" of "f" incompatible with return type "None" in supertype "A"
 class D(A):
diff --git a/test-data/unit/check-dynamic-typing.test b/test-data/unit/check-dynamic-typing.test
--- a/test-data/unit/check-dynamic-typing.test
+++ b/test-data/unit/check-dynamic-typing.test
@@ -704,7 +704,9 @@ class C:
 class B(C):
     def f(self, a): pass
 class A(B):
-    def f(self, a: 'D') -> None: # E: Argument 1 of "f" is incompatible with supertype "C"; supertype defines the argument type as "A"
+    def f(self, a: 'D') -> None: # E: Argument 1 of "f" is incompatible with supertype "C"; supertype defines the argument type as "A" \
+                                 # N: This violates the Liskov substitution principle \
+                                 # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
         pass
 class D: pass
 [out]
diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test
--- a/test-data/unit/check-errorcodes.test
+++ b/test-data/unit/check-errorcodes.test
@@ -279,7 +279,9 @@ class D:
     def f(self, x: int) -> int:
         return 0
 class E(D):
-    def f(self, x: str) -> int:  # E: Argument 1 of "f" is incompatible with supertype "D"; supertype defines the argument type as "int"  [override]
+    def f(self, x: str) -> int:  # E: Argument 1 of "f" is incompatible with supertype "D"; supertype defines the argument type as "int"  [override] \
+                                 # N: This violates the Liskov substitution principle \
+                                 # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
         return 0
 
 class O:
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -46,7 +46,10 @@ class A(object):
     def f(self, a: int, b: str) -> None: pass
 
 class B(A):
-    def f(self, b: str, a: int) -> None: pass # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" # E: Argument 2 of "f" is incompatible with supertype "A"; supertype defines the argument type as "str"
+    def f(self, b: str, a: int) -> None: 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 \
+                                              # E: Argument 2 of "f" is incompatible with supertype "A"; supertype defines the argument type as "str" 
 
 class C(A):
     def f(self, foo: int, bar: str) -> None: pass
diff --git a/test-data/unit/check-generic-subtyping.test b/test-data/unit/check-generic-subtyping.test
--- a/test-data/unit/check-generic-subtyping.test
+++ b/test-data/unit/check-generic-subtyping.test
@@ -189,7 +189,9 @@ class C: pass
 class D: pass
 class A(B[C]):
     def f(self, a: D) -> None: pass \
-        # E: Argument 1 of "f" is incompatible with supertype "B"; supertype defines the argument type as "C"
+        # E: Argument 1 of "f" is incompatible with supertype "B"; supertype defines the argument type as "C" \
+        # N: This violates the Liskov substitution principle \
+        # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
     def g(self, a: C) -> None: pass
 [out]
 
@@ -202,7 +204,9 @@ class B:
     def g(self, a: C) -> None: pass
 class A(B, Generic[T]):
     def f(self, a: T) -> None: pass \
-        # E: Argument 1 of "f" is incompatible with supertype "B"; supertype defines the argument type as "C"
+        # E: Argument 1 of "f" is incompatible with supertype "B"; supertype defines the argument type as "C" \
+        # N: This violates the Liskov substitution principle \
+        # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
     def g(self, a: 'C') -> None: pass
 [out]
 
@@ -215,7 +219,9 @@ class B(Generic[T]):
     def g(self, a: T) -> None: pass
 class A(B[S], Generic[T, S]):
     def f(self, a: T) -> None: pass \
-        # E: Argument 1 of "f" is incompatible with supertype "B"; supertype defines the argument type as "S"
+        # E: Argument 1 of "f" is incompatible with supertype "B"; supertype defines the argument type as "S" \
+        # N: This violates the Liskov substitution principle \
+        # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
     def g(self, a: S) -> None: pass
 [out]
 
@@ -233,7 +239,9 @@ class C(Generic[T, U, V]):
 class B(C[D, D, T], Generic[T]): pass
 class A(B[S], Generic[T, S]):
     def f(self, a: T) -> None: pass \
-        # E: Argument 1 of "f" is incompatible with supertype "C"; supertype defines the argument type as "S"
+        # E: Argument 1 of "f" is incompatible with supertype "C"; supertype defines the argument type as "S" \
+        # N: This violates the Liskov substitution principle \
+        # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
     def g(self, a: S) -> None: pass
 [out]
 
@@ -283,6 +291,8 @@ class D(A):
     def f(self, x: T1, y: S) -> None: pass # TODO: This error could be more specific.
 [out]
 main:12: error: Argument 2 of "f" is incompatible with supertype "A"; supertype defines the argument type as "S"
+main:12: note: This violates the Liskov substitution principle
+main:12: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 main:14: error: Signature of "f" incompatible with supertype "A"
 
 
@@ -538,7 +548,9 @@ class I(Generic[T]):
     def g(self, a: T) -> None: pass
 class A(I[C]):
     def f(self, a: 'D') -> None: pass \
-        # E: Argument 1 of "f" is incompatible with supertype "I"; supertype defines the argument type as "C"
+        # E: Argument 1 of "f" is incompatible with supertype "I"; supertype defines the argument type as "C" \
+        # N: This violates the Liskov substitution principle \
+        # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
     def g(self, a: 'C') -> None: pass
 class C: pass
 class D: pass
@@ -570,7 +582,9 @@ class B(I[C]):
     def g(self, a: 'C', b: Any) -> None: pass
 class A(B):
     def g(self, a: 'C', b: 'C') -> None: pass \
-        # E: Argument 2 of "g" is incompatible with supertype "I"; supertype defines the argument type as "D"
+        # E: Argument 2 of "g" is incompatible with supertype "I"; supertype defines the argument type as "D" \
+        # N: This violates the Liskov substitution principle \
+        # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
     def f(self, a: 'C', b: 'C') -> None: pass
 class C: pass
 class D: pass
@@ -587,7 +601,9 @@ class B(I[C]):
     def f(self, a: 'C', b: Any) -> None: pass
 class A(B):
     def f(self, a: 'C', b: 'D') -> None: pass \
-        # E: Argument 2 of "f" is incompatible with supertype "I"; supertype defines the argument type as "C"
+        # E: Argument 2 of "f" is incompatible with supertype "I"; supertype defines the argument type as "C" \
+        # N: This violates the Liskov substitution principle \
+        # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 class C: pass
 class D: pass
 [out]
diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test
--- a/test-data/unit/check-multiple-inheritance.test
+++ b/test-data/unit/check-multiple-inheritance.test
@@ -539,6 +539,8 @@ class IntFlag(int, Flag):
     def __or__(self: _T, other: str) -> _T: ...
 [out]
 main:8: error: Argument 1 of "__or__" is incompatible with supertype "Flag"; supertype defines the argument type as "IntFlag"
+main:8: note: This violates the Liskov substitution principle
+main:8: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 
 [case testMultipleInheritance_MethodDefinitionsCompatibleNoOverride]
 from typing import TypeVar, Union