Repository: python/mypy

Issue:
Feature request: Disallow final classes from having abstract methods or properties
Currently, mypy checks whether a class is abstract when it's instantiated. For classes with the `@final` decorator, it would make sense for there to be a typing error if they define any abstract methods or properties, since those could never be implemented.

It would also make sense for classes that explicitly have the `ABCMeta` metaclass and also have the `@final` decorator to be a typing error if the class contains any instance methods or properties, since there could never be an instance created to call them with.

Note that `abstract final` classes are an idiom that is sometimes used in Java to define a class that is just a grouping of static methods, but I believe that's mostly a result of Java not having modules and top-level functions. In Python, one could organize top-level functions and group them with a module. This could be an argument to disallow a class to have both `@final` and `ABCMeta` all-together.


Discussion:
I like this idea. It is also easy to implement, one would just need to add a check at the end of `calculate_class_abstract_status()` giving an error if the class is final but the lost of abstract attributes is non-empty.

Resolving patch:
diff --git a/mypy/semanal_classprop.py b/mypy/semanal_classprop.py
--- a/mypy/semanal_classprop.py
+++ b/mypy/semanal_classprop.py
@@ -109,6 +109,10 @@ def report(message: str, severity: str) -> None:
             report("Class {} has abstract attributes {}".format(typ.fullname, attrs), 'error')
             report("If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass",
                    'note')
+    if typ.is_final and abstract:
+        attrs = ", ".join('"{}"'.format(attr) for attr in sorted(abstract))
+        errors.report(typ.line, typ.column,
+                      "Final class {} has abstract attributes {}".format(typ.fullname, attrs))
 
 
 def check_protocol_status(info: TypeInfo, errors: Errors) -> None:


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
@@ -6677,3 +6677,50 @@ class B:
                                         # N: Perhaps you need "Callable[...]" or a callback protocol?
 
 [builtins fixtures/classmethod.pyi]
+
+[case testFinalClassWithAbstractAttributes]
+from abc import abstractmethod, ABCMeta
+from typing import final
+
+@final
+class A(metaclass=ABCMeta):  # E: Final class __main__.A has abstract attributes "bar", "foo"
+    @abstractmethod
+    def foo(self):
+        pass
+
+    @property
+    @abstractmethod
+    def bar(self):
+        pass
+
+[builtins fixtures/property.pyi]
+
+[case testFinalClassWithoutABCMeta]
+from abc import abstractmethod
+from typing import final
+
+@final
+class A():  # E: Final class __main__.A has abstract attributes "bar", "foo"
+    @abstractmethod
+    def foo(self):
+        pass
+
+    @property
+    @abstractmethod
+    def bar(self):
+        pass
+
+[builtins fixtures/property.pyi]
+
+[case testFinalClassInheritedAbstractAttributes]
+from abc import abstractmethod, ABCMeta
+from typing import final
+
+class A(metaclass=ABCMeta):
+    @abstractmethod
+    def foo(self):
+        pass
+
+@final
+class B(A):  # E: Final class __main__.B has abstract attributes "foo"
+    pass