Repository: python/mypy

Issue:
Protocols with self break super
**Bug Report**

Sometimes there's a need to use a `typing.Protocol` to specify `self` typing for cases like mixin classes, where the methods aren't there at class definition, but will be there at runtime, once the mixin is inherited from. This generally works fine, except it breaks in one silly case, a short repro of which is available below.

In short, it looks like `super` doesn't like protocols as it's second argument, which isn't even explicitly passed here.

**To Reproduce**

```py
from typing import Protocol

class MixinProt(Protocol):
    def func(self) -> None:
        ...

class Mixin:
    def func(self: MixinProt) -> None:
        return super().func()
```

**Expected Behavior**

No error, duh.

**Actual Behavior**

From the very last line of the code above:
```
Argument 2 for "super" not an instance of argument 1
```

**Your Environment**

- Mypy version used: 0.931
- Mypy command-line flags: none
- Mypy configuration options from `mypy.ini` (and other config files): none
- Python version used: 3.8.10
- Operating system and version: Win7x64


Discussion:
FYI, PyLance seems to handle this correctly; I'm not seeing this mypy error in VSCode. MyPy still does this in 0.891.

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -4316,8 +4316,18 @@ def visit_super_expr(self, e: SuperExpr) -> Type:
                     mro = e.info.mro
                     index = mro.index(type_info)
         if index is None:
-            self.chk.fail(message_registry.SUPER_ARG_2_NOT_INSTANCE_OF_ARG_1, e)
-            return AnyType(TypeOfAny.from_error)
+            if (
+                instance_info.is_protocol
+                and instance_info != type_info
+                and not type_info.is_protocol
+            ):
+                # A special case for mixins, in this case super() should point
+                # directly to the host protocol, this is not safe, since the real MRO
+                # is not known yet for mixin, but this feature is more like an escape hatch.
+                index = -1
+            else:
+                self.chk.fail(message_registry.SUPER_ARG_2_NOT_INSTANCE_OF_ARG_1, e)
+                return AnyType(TypeOfAny.from_error)
 
         if len(mro) == index + 1:
             self.chk.fail(message_registry.TARGET_CLASS_HAS_NO_BASE_CLASS, e)


Test patch:
diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test
--- a/test-data/unit/check-selftype.test
+++ b/test-data/unit/check-selftype.test
@@ -792,6 +792,26 @@ reveal_type(f.copy())  # N: Revealed type is "__main__.File"
 b.copy()  # E: Invalid self argument "Bad" to attribute function "copy" with type "Callable[[T], T]"
 [builtins fixtures/tuple.pyi]
 
+[case testMixinProtocolSuper]
+from typing import Protocol
+
+class Base(Protocol):
+    def func(self) -> int:
+        ...
+
+class TweakFunc:
+    def func(self: Base) -> int:
+        return reveal_type(super().func())  # N: Revealed type is "builtins.int"
+
+class Good:
+    def func(self) -> int: ...
+class C(TweakFunc, Good): pass
+C().func()  # OK
+
+class Bad:
+    def func(self) -> str: ...
+class CC(TweakFunc, Bad): pass  # E: Definition of "func" in base class "TweakFunc" is incompatible with definition in base class "Bad"
+
 [case testBadClassLevelDecoratorHack]
 from typing_extensions import Protocol
 from typing import TypeVar, Any