Repository: python/mypy

Issue:
Decorated method type-incompatibility with superclass
I'm raising a bug.

It's also posted on SO at https://stackoverflow.com/q/52744713/5986907

I get 

    $ mypy file.py
    python.py:34: error: Signature of "fn" incompatible with supertype "B"

for file.py

    from typing import Callable, TypeVar, cast

    T = TypeVar('T')


    def dec(f: Callable[..., T]) -> T:
        def p(self):
            return getattr(self, '_' + f.__name__)

        return cast(T, property(p))


    class X:
        pass


    class Y(X):
        pass


    class A:
        _fn = Y()


    class B(A):
        @dec
        def fn(self) -> X:
            return X()


    class C(B):
        @dec
        def fn(self) -> Y:
            return Y()


I'd expect there to be no error as Y is a subclass of X.

I've tried this with mypy 0.630 and Python 3.5.2, 3.6.6 and 3.7.0. Also with mypy git master for python 3.5.2 and 3.7.0

It's possible these are related but I couldn't quite connect them
https://github.com/python/mypy/issues/220
https://github.com/python/mypy/issues/4430


Discussion:
Hm, it still fails on master, this is because of this code:
```python
            elif is_equivalent(original_type, typ):
                # Assume invariance for a non-callable attribute here. Note
                # that this doesn't affect read-only properties which can have
                # covariant overrides.
                #
                # TODO: Allow covariance for read-only attributes?
                pass
            else:
                # ...give error
```
I think this is easy to fix by replacing `is_equivalent` with `is_subtype` at least for effectively read-only attributes. Anyway mypy currently allows covariant overriding for mutable attributes although this is technically unsafe, see https://github.com/python/mypy/issues/3208

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1439,7 +1439,7 @@ def check_method_or_accessor_override_for_base(self, defn: Union[FuncDef,
                     self.msg.cant_override_final(name, base.name, defn)
                 # Second, final can't override anything writeable independently of types.
                 if defn.is_final:
-                    self.check_no_writable(name, base_attr.node, defn)
+                    self.check_if_final_var_override_writable(name, base_attr.node, defn)
 
             # Check the type of override.
             if name not in ('__init__', '__new__', '__init_subclass__'):
@@ -1534,7 +1534,10 @@ def check_method_override_for_base_with_name(
                 # that this doesn't affect read-only properties which can have
                 # covariant overrides.
                 #
-                # TODO: Allow covariance for read-only attributes?
+                pass
+            elif (base_attr.node and not self.is_writable_attribute(base_attr.node)
+                  and is_subtype(typ, original_type)):
+                # If the attribute is read-only, allow covariance
                 pass
             else:
                 self.msg.signature_incompatible_with_supertype(
@@ -1920,7 +1923,7 @@ class C(B, A[int]): ...  # this is unsafe because...
         if is_final_node(second.node):
             self.msg.cant_override_final(name, base2.name, ctx)
         if is_final_node(first.node):
-            self.check_no_writable(name, second.node, ctx)
+            self.check_if_final_var_override_writable(name, second.node, ctx)
         # __slots__ is special and the type can vary across class hierarchy.
         if name == '__slots__':
             ok = True
@@ -2385,10 +2388,14 @@ def check_compatibility_final_super(self, node: Var,
             self.msg.cant_override_final(node.name, base.name, node)
             return False
         if node.is_final:
-            self.check_no_writable(node.name, base_node, node)
+            self.check_if_final_var_override_writable(node.name, base_node, node)
         return True
 
-    def check_no_writable(self, name: str, base_node: Optional[Node], ctx: Context) -> None:
+    def check_if_final_var_override_writable(self,
+                                             name: str,
+                                             base_node:
+                                             Optional[Node],
+                                             ctx: Context) -> None:
         """Check that a final variable doesn't override writeable attribute.
 
         This is done to prevent situations like this:
@@ -2400,14 +2407,10 @@ class D(C):
             x: C = D()
             x.attr = 3  # Oops!
         """
-        if isinstance(base_node, Var):
-            ok = False
-        elif isinstance(base_node, OverloadedFuncDef) and base_node.is_property:
-            first_item = cast(Decorator, base_node.items[0])
-            ok = not first_item.var.is_settable_property
-        else:
-            ok = True
-        if not ok:
+        writable = True
+        if base_node:
+            writable = self.is_writable_attribute(base_node)
+        if writable:
             self.msg.final_cant_override_writable(name, ctx)
 
     def get_final_context(self) -> bool:
@@ -4866,6 +4869,16 @@ def conditional_type_map_with_intersection(self,
         new_yes_type = make_simplified_union(out)
         return {expr: new_yes_type}, {}
 
+    def is_writable_attribute(self, node: Node) -> bool:
+        """Check if an attribute is writable"""
+        if isinstance(node, Var):
+            return True
+        elif isinstance(node, OverloadedFuncDef) and node.is_property:
+            first_item = cast(Decorator, node.items[0])
+            return first_item.var.is_settable_property
+        else:
+            return False
+
 
 def conditional_type_map(expr: Expression,
                          current_type: Optional[Type],


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
@@ -512,6 +512,28 @@ class B(A):
     def h(cls) -> int: pass
 [builtins fixtures/classmethod.pyi]
 
+[case testAllowCovarianceInReadOnlyAttributes]
+from typing import Callable, TypeVar
+
+T = TypeVar('T')
+
+class X:
+    pass
+
+
+class Y(X):
+    pass
+
+def dec(f: Callable[..., T]) -> T: pass
+
+class A:
+    @dec
+    def f(self) -> X: pass
+
+class B(A):
+    @dec
+    def f(self) -> Y: pass
+
 
 -- Constructors
 -- ------------