Repository: python/mypy

Issue:
Shouldn't allow overriding a class method with an instance method
See #1866. We had a bug where a subclass had omitted the `@classmethod` decorator on a method it was overriding. Yet mypy did not flag this as an error. Smaller repro:

``` python
class C:
    @classmethod
    def foo(cls) -> None: pass
class D(C):
    def foo(self) -> None: pass
```

This code is IMO unsound in the presence of Type[C] -- D is a subclass of C but we cannot call .foo() on the subclass without an instance. (The actual example didn't use Type[C], it used dynamic dispatch.)



Discussion:
Guido suspects there's a reason this isn't already an error -- so there may be discussion needed here once someone digs in to understand it better.

It's not a very strong suspicion though. It could just be an oversight;
maybe class methods were added later and this was missed. Let's fix this
and see what it catches.

--Guido (mobile)

I have a fix for this, but the stdlib actually uses this. importlib has numerous classmethods that override non-classmethods (for example, in [importlib/machinery.pyi](https://github.com/python/typeshed/blob/master/stdlib/3/importlib/machinery.pyi), all of those subclass [MetaPathFinder](https://github.com/python/typeshed/blob/master/stdlib/3/importlib/abc.pyi) which doesn't have classmethods.) Though looking at the actual [CPython implementation](https://github.com/python/cpython/blob/master/Lib/importlib/_bootstrap.py), I don't see them _actually_ subclassing the ABCs, so it might be a typeshed mistake? I'd appreciate some guidance here.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -907,6 +907,13 @@ def check_override(self, override: FunctionLike, original: FunctionLike,
             # this could be unsafe with reverse operator methods.
             fail = True
 
+        if isinstance(original, CallableType) and isinstance(override, CallableType):
+            if (isinstance(original.definition, FuncItem) and
+                    isinstance(override.definition, FuncItem)):
+                if ((original.definition.is_static or original.definition.is_class) and
+                        not (override.definition.is_static or override.definition.is_class)):
+                    fail = True
+
         if fail:
             emitted_msg = False
             if (isinstance(override, CallableType) and


Test patch:
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
@@ -809,6 +809,23 @@ class C:
       pass
 [builtins fixtures/classmethod.pyi]
 
+[case testClassMethodSubclassing]
+class A:
+    @classmethod
+    def f(cls) -> None: pass
+
+    def g(self) -> None: pass
+
+class B(A):
+    def f(self) -> None: pass  # E: Signature of "f" incompatible with supertype "A"
+
+    @classmethod
+    def g(cls) -> None: pass
+
+class C(A):
+    @staticmethod
+    def f() -> None: pass
+[builtins fixtures/classmethod.pyi]
 
 -- Properties
 -- ----------
@@ -2700,6 +2717,26 @@ class B(A):
 [out]
 main:8: error: Incompatible types in assignment (expression has type Callable[[str], None], base class "A" defined the type as Callable[[int], None])
 
+[case testClassStaticMethodSubclassing]
+class A:
+    @staticmethod
+    def a() -> None: pass
+
+    def b(self) -> None: pass
+
+    @staticmethod
+    def c() -> None: pass
+
+class B(A):
+    def a(self) -> None: pass  # E: Signature of "a" incompatible with supertype "A"
+
+    @classmethod
+    def b(cls) -> None: pass
+
+    @staticmethod
+    def c() -> None: pass
+[builtins fixtures/classmethod.pyi]
+
 [case testTempNode]
 class A():
     def a(self) -> None: pass