Repository: python/mypy (Python)

Issue:
Confusing error about __attrs_attrs__ when using multiple inheritance
This code now generates an error about class AB:

```py
import attr

@attr.s
class A:
    x = attr.ib(type=int)

@attr.s
class B:
    y = attr.ib(type=int)

# Definition of "__attrs_attrs__" in base class "A" is incompatible with definition in base class "B"
class AB(A, B):
    pass
```

The code is arguably questionable, and the error goes away if we decorate AB with `@attr.s`, but that's not clear from the error message. It would be better to add a note, such as something like this:

```
... :note: Should class "AB" use the @attr.s decorator?
```

This started happening after #11794, and it might be considered a minor regression because of the confusing message.

(Behind the scenes, a `__attrs_attrs__` attribute gets generated with a class-specific named tuple as the type. These named tuples aren't compatible with each other.)

cc @Tinche 



Hints:
Interesting, I can take a look at this.
In runtime,

```
>>> AB.__attrs_attrs__
(Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None),)
```
So I'm guessing it just follows the MRO and gets the class attribute from A.

Here's the equivalent situation without attrs:

```
class C:
    _test_: str = "a"


class D:
    _test_: int = 1


class CD(C, D):
    pass
```
This is a Mypy error (`error: Definition of "_test_" in base class "C" is incompatible with definition in base class "D"`).

So if we want to keep this behavior and still raise an error, the note approach is the way to go?
Yeah, the note approach seems fine. If you don't use `@attr.s` in the derived class, the inherited `__init__` doesn't initialize all the attributes, the value of `__attrs_attrs__` won't include all attributes, etc.
This feels similar to #12008. (Does attrs autocreate `__match_args__`?)
> This feels similar to #12008. (Does attrs autocreate `__match_args__`?)

It does.

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -2103,8 +2103,9 @@ class C(B, A[int]): ...  # this is unsafe because...
             self.msg.cant_override_final(name, base2.name, ctx)
         if is_final_node(first.node):
             self.check_if_final_var_override_writable(name, second.node, ctx)
-        # __slots__ and __deletable__ are special and the type can vary across class hierarchy.
-        if name in ('__slots__', '__deletable__'):
+        # Some attributes like __slots__ and __deletable__ are special, and the type can
+        # vary across class hierarchy.
+        if isinstance(second.node, Var) and second.node.allow_incompatible_override:
             ok = True
         if not ok:
             self.msg.base_class_definitions_incompatible(name, base1, base2,
@@ -2460,16 +2461,12 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[
             last_immediate_base = direct_bases[-1] if direct_bases else None
 
             for base in lvalue_node.info.mro[1:]:
-                # Only check __slots__ against the 'object'
-                # If a base class defines a Tuple of 3 elements, a child of
-                # this class should not be allowed to define it as a Tuple of
-                # anything other than 3 elements. The exception to this rule
-                # is __slots__, where it is allowed for any child class to
-                # redefine it.
-                if lvalue_node.name == "__slots__" and base.fullname != "builtins.object":
-                    continue
-                # We don't care about the type of "__deletable__".
-                if lvalue_node.name == "__deletable__":
+                # The type of "__slots__" and some other attributes usually doesn't need to
+                # be compatible with a base class. We'll still check the type of "__slots__"
+                # against "object" as an exception.
+                if (isinstance(lvalue_node, Var) and lvalue_node.allow_incompatible_override and
+                        not (lvalue_node.name == "__slots__" and
+                             base.fullname == "builtins.object")):
                     continue
 
                 if is_private(lvalue_node.name):
diff --git a/mypy/nodes.py b/mypy/nodes.py
--- a/mypy/nodes.py
+++ b/mypy/nodes.py
@@ -853,7 +853,7 @@ def deserialize(cls, data: JsonDict) -> 'Decorator':
     'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import',
     'is_classvar', 'is_abstract_var', 'is_final', 'final_unset_in_class', 'final_set_in_init',
     'explicit_self_type', 'is_ready', 'from_module_getattr',
-    'has_explicit_value',
+    'has_explicit_value', 'allow_incompatible_override',
 ]
 
 
@@ -885,6 +885,7 @@ class Var(SymbolNode):
                  'explicit_self_type',
                  'from_module_getattr',
                  'has_explicit_value',
+                 'allow_incompatible_override',
                  )
 
     def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None:
@@ -932,6 +933,8 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None:
         # Var can be created with an explicit value `a = 1` or without one `a: int`,
         # we need a way to tell which one is which.
         self.has_explicit_value = False
+        # If True, subclasses can override this with an incompatible type.
+        self.allow_incompatible_override = False
 
     @property
     def name(self) -> str:
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -744,6 +744,7 @@ def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext',
     var.info = ctx.cls.info
     var.is_classvar = True
     var._fullname = f"{ctx.cls.fullname}.{MAGIC_ATTR_CLS_NAME}"
+    var.allow_incompatible_override = True
     ctx.cls.info.names[MAGIC_ATTR_NAME] = SymbolTableNode(
         kind=MDEF,
         node=var,
@@ -778,7 +779,6 @@ def _add_match_args(ctx: 'mypy.plugin.ClassDefContext',
             cls=ctx.cls,
             name='__match_args__',
             typ=match_args,
-            final=True,
         )
 
 
diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py
--- a/mypy/plugins/common.py
+++ b/mypy/plugins/common.py
@@ -5,7 +5,7 @@
     FuncDef, PassStmt, RefExpr, SymbolTableNode, Var, JsonDict,
 )
 from mypy.plugin import CheckerPluginInterface, ClassDefContext, SemanticAnalyzerPluginInterface
-from mypy.semanal import set_callable_name
+from mypy.semanal import set_callable_name, ALLOW_INCOMPATIBLE_OVERRIDE
 from mypy.types import (
     CallableType, Overloaded, Type, TypeVarType, deserialize_type, get_proper_type,
 )
@@ -163,6 +163,7 @@ def add_attribute_to_class(
         typ: Type,
         final: bool = False,
         no_serialize: bool = False,
+        override_allow_incompatible: bool = False,
 ) -> None:
     """
     Adds a new attribute to a class definition.
@@ -180,6 +181,10 @@ def add_attribute_to_class(
     node = Var(name, typ)
     node.info = info
     node.is_final = final
+    if name in ALLOW_INCOMPATIBLE_OVERRIDE:
+        node.allow_incompatible_override = True
+    else:
+        node.allow_incompatible_override = override_allow_incompatible
     node._fullname = info.fullname + '.' + name
     info.names[name] = SymbolTableNode(
         MDEF,
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -227,7 +227,7 @@ def transform(self) -> None:
             literals: List[Type] = [LiteralType(attr.name, str_type)
                         for attr in attributes if attr.is_in_init]
             match_args_type = TupleType(literals, ctx.api.named_type("builtins.tuple"))
-            add_attribute_to_class(ctx.api, ctx.cls, "__match_args__", match_args_type, final=True)
+            add_attribute_to_class(ctx.api, ctx.cls, "__match_args__", match_args_type)
 
         self._add_dataclass_fields_magic_attribute()
 
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -152,6 +152,10 @@
 # available very early on.
 CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"]
 
+# Subclasses can override these Var attributes with incompatible types. This can also be
+# set for individual attributes using 'allow_incompatible_override' of Var.
+ALLOW_INCOMPATIBLE_OVERRIDE: Final = ('__slots__', '__deletable__', '__match_args__')
+
 
 # Used for tracking incomplete references
 Tag: _TypeAlias = int
@@ -2915,18 +2919,20 @@ def make_name_lvalue_var(
         self, lvalue: NameExpr, kind: int, inferred: bool, has_explicit_value: bool,
     ) -> Var:
         """Return a Var node for an lvalue that is a name expression."""
-        v = Var(lvalue.name)
+        name = lvalue.name
+        v = Var(name)
         v.set_line(lvalue)
         v.is_inferred = inferred
         if kind == MDEF:
             assert self.type is not None
             v.info = self.type
             v.is_initialized_in_class = True
+            v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_OVERRIDE
         if kind != LDEF:
-            v._fullname = self.qualified_name(lvalue.name)
+            v._fullname = self.qualified_name(name)
         else:
             # fullanme should never stay None
-            v._fullname = lvalue.name
+            v._fullname = name
         v.is_ready = False  # Type not inferred yet
         v.has_explicit_value = has_explicit_value
         return v


Test patch:
diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test
--- a/test-data/unit/check-attr.test
+++ b/test-data/unit/check-attr.test
@@ -1539,3 +1539,19 @@ n: NoMatchArgs
 reveal_type(n.__match_args__)  # E: "NoMatchArgs" has no attribute "__match_args__" \
                                # N: Revealed type is "Any"
 [builtins fixtures/attr.pyi]
+
+[case testAttrsMultipleInheritance]
+# flags: --python-version 3.10
+import attr
+
+@attr.s
+class A:
+    x = attr.ib(type=int)
+
+@attr.s
+class B:
+    y = attr.ib(type=int)
+
+class AB(A, B):
+    pass
+[builtins fixtures/attr.pyi]
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -1536,3 +1536,19 @@ A(a=1, b=2)
 A(1)
 A(a="foo")  # E: Argument "a" to "A" has incompatible type "str"; expected "int"
 [builtins fixtures/dataclasses.pyi]
+
+[case testDataclassesMultipleInheritanceWithNonDataclass]
+# flags: --python-version 3.10
+from dataclasses import dataclass
+
+@dataclass
+class A:
+    prop_a: str
+
+@dataclass
+class B:
+    prop_b: bool
+
+class Derived(A, B):
+    pass
+[builtins fixtures/dataclasses.pyi]