Repository: python/mypy
Pull request: Allow classes as protocol implementations
Fixes #4536

This use case is specified by PEP 544 but was not implemented. Only instances (not class objects) were allowed as protocol implementations. The PR is quite straightforward, essentially I just pass a `class_obj` flag everywhere to know whether we need or not to bind self in a method.

cc @JukkaL 

Problem statement:
Protocols should not care if a type is a class or an instance
As a developer of a protocol, I don't care whether that protocol is met by an instance or a class, as long as the necessary functions and variables are present and correct.  Currently it appears that only instances can satisfy a protocol.

For example, the code below attempts to meet a protocol using an instance and a class.  It executes in python fine, but mypy complains that `Type[Jumper1]` is not compatible with the `Jumps` protocol:

```python
from typing_extensions import Protocol

class Jumps(Protocol):
    def jump(self) -> int:
        pass

class Jumper1:
    @classmethod
    def jump(cls) -> int:
        print("class jumping")
        return 1

class Jumper2:
    def jump(self) -> int:
        print("instance jumping")
        return 2

def do_jump(j: Jumps):
    print(j.jump())

do_jump(Jumper1)
do_jump(Jumper2())
```

Hints from issue discussion:
I just hit this as well. It looks like this and #5018 are the same issue, but about modules rather than classes.

I think the same thing probably should be true of instances as well.  If I do, for example:

```
class Empty: pass
nothing = Empty()
def free_jump() -> int:
    return 3
nothing.jump = free_jump
do_jump(nothing)
```

I feel like that ought to work.

Zope Interface has this notion of "provides" as distinct from "implements" to draw a clean separation between the different ways a type can relate to an Interface; it seems like Protocol should behave in the same way.  It has a `directlyProvides` function which can be used to annotate any object, including classes and modules, to indicate that it provides the interface.
I agree with the OP's initial example (and with #5018), but I think Glyph's example goes a bit too far -- it dynamically modifies an object. While in the toy example it is easy to see that nothing has a jump attribute, in realistic applications I doubt that in most cases mypy would be able to make the connection.

IIUC the provides/implements distinction is between a class whose instances can jump() vs. an instance that can jump()? (Though I'm not all that familiar with Zope Interface, and the two words don't have clearly distinct meanings to me -- I could easily have it backwards.)
> I agree with the OP's initial example (and with #5018), but I think Glyph's example goes a bit too far

The same for me.
Note to self: we need to add an OP example (simplified) to the PEP 544 (there is already a PR for modules).
> IIUC the provides/implements distinction is between a class whose instances can jump() vs. an instance that can jump()?

Yes.

To restate:

An object `x` is said to "provide" the interface `IX` if a function that "takes an `IX`" can take `x` and behave correctly; i.e. it matches the specification presented by `IX`.

A type `X` is said to "implement" the interface `IX` if all its instances provide `IX`.

The distinction is important because if `IX` specifies methods `a`, `b`, and `c`, the type `X` might:

- have all three of those attributes just because it implements the interface, but this of course doesn't imply that it provides said interface,
- implement the interface using some runtime trickery and not itself have any of those attributes,
- have all three of those attributes because it directly *provides* the interface; if it does so, depending on descriptor semantics, its instances may *or may not* provide the interface; i.e. it may provide but not implement the interface.
Regarding my setting-attributes-on-an-instance example, I agree that as stated it's perhaps pushing mypy a bit far.  Practically I do not expect it to be able to do that automatically without help.  What I want is maybe something like `dynamic_cast` in C++, for `@runtime` `Protocol`s only.
OK, so "provides" is a property of objects while "implements" is a property of types -- with the understanding that a class is both an object and a type. And the distinction is an important once especially when you're doing runtime checking and all you can check is the presence of specific attributes, rather than the full signatures of methods.

(I've got to admit that even after writing that down the choice of terms still feels a bit arbitrary. Especially "implements" could go either way in my intuition.)

In general the checks that mypy performs are about "provides" -- e.g. we have a function foo:
```py
class P(Protocol): ...
def foo(arg: P):
    ...
```
and we're calling it with an expression:
```py
foo(expr)
```
then mypy checks whether `expr` provides `P`. In the usual case, this boils down to checking whether the *type* of `expr` *implements* `P`.

But when `expr` is a class object, checking whether that class provides `P` roughly requires looking for matches of `P`'s methods among that class's class methods and static methods. (We should first check whether its metaclass implements `P`, given that classes are objects.)

Finally when `expr` is a module object we should again check whether the module provides `P` by matching the functions it defines against the methods of `P` (again after checking whether `ModuleType` might implement `P`).

I suppose in the wonderful world of Zope Interfaces there are other cases to consider, since providing an interface could be done dynamically. But mypy is no help here, and I think the examples of dynamically providing an interface that I've seen all deserve the term "hack". :-)
A small note about this, mypy should accept not only `Type[X]` as an implementation, but also a `Callable`, if the fallback is appropriate. The latter actually currently works but only partially, in particular `__call__` is lost when switching to the fallback instance.

Also I raise priority to high, since this is a required part of PEP 544 in its accepted version.
Seeing how the issue is still open, and how the described behaviour is still observed, I have to ask: are there any known workarounds for this outside of completely disabling typechecks for the relevant code parts?
Total redesign involving getting rid of the classmethods is not always an option.
Bumping this as I just ran into it as well, any workarounds?

Commit message: Allow classes as protocol implementations

Patch to review:
diff --git a/mypy/messages.py b/mypy/messages.py
index 99ff6d5a3703..b38ba60a89fa 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -743,7 +743,9 @@ def incompatible_argument_note(
         context: Context,
         code: ErrorCode | None,
     ) -> None:
-        if isinstance(original_caller_type, (Instance, TupleType, TypedDictType)):
+        if isinstance(
+            original_caller_type, (Instance, TupleType, TypedDictType, TypeType, CallableType)
+        ):
             if isinstance(callee_type, Instance) and callee_type.type.is_protocol:
                 self.report_protocol_problems(
                     original_caller_type, callee_type, context, code=code
@@ -1791,7 +1793,7 @@ def impossible_intersection(
 
     def report_protocol_problems(
         self,
-        subtype: Instance | TupleType | TypedDictType,
+        subtype: Instance | TupleType | TypedDictType | TypeType | CallableType,
         supertype: Instance,
         context: Context,
         *,
@@ -1811,15 +1813,15 @@ def report_protocol_problems(
         exclusions: dict[type, list[str]] = {
             TypedDictType: ["typing.Mapping"],
             TupleType: ["typing.Iterable", "typing.Sequence"],
-            Instance: [],
         }
-        if supertype.type.fullname in exclusions[type(subtype)]:
+        if supertype.type.fullname in exclusions.get(type(subtype), []):
             return
         if any(isinstance(tp, UninhabitedType) for tp in get_proper_types(supertype.args)):
             # We don't want to add notes for failed inference (e.g. Iterable[<nothing>]).
             # This will be only confusing a user even more.
             return
 
+        class_obj = False
         if isinstance(subtype, TupleType):
             if not isinstance(subtype.partial_fallback, Instance):
                 return
@@ -1828,6 +1830,21 @@ def report_protocol_problems(
             if not isinstance(subtype.fallback, Instance):
                 return
             subtype = subtype.fallback
+        elif isinstance(subtype, TypeType):
+            if not isinstance(subtype.item, Instance):
+                return
+            class_obj = True
+            subtype = subtype.item
+        elif isinstance(subtype, CallableType):
+            if not subtype.is_type_obj():
+                return
+            ret_type = get_proper_type(subtype.ret_type)
+            if isinstance(ret_type, TupleType):
+                ret_type = ret_type.partial_fallback
+            if not isinstance(ret_type, Instance):
+                return
+            class_obj = True
+            subtype = ret_type
 
         # Report missing members
         missing = get_missing_protocol_members(subtype, supertype)
@@ -1836,20 +1853,29 @@ def report_protocol_problems(
             and len(missing) < len(supertype.type.protocol_members)
             and len(missing) <= MAX_ITEMS
         ):
-            self.note(
-                '"{}" is missing following "{}" protocol member{}:'.format(
-                    subtype.type.name, supertype.type.name, plural_s(missing)
-                ),
-                context,
-                code=code,
-            )
-            self.note(", ".join(missing), context, offset=OFFSET, code=code)
+            if missing == ["__call__"] and class_obj:
+                self.note(
+                    '"{}" has constructor incompatible with "__call__" of "{}"'.format(
+                        subtype.type.name, supertype.type.name
+                    ),
+                    context,
+                    code=code,
+                )
+            else:
+                self.note(
+                    '"{}" is missing following "{}" protocol member{}:'.format(
+                        subtype.type.name, supertype.type.name, plural_s(missing)
+                    ),
+                    context,
+                    code=code,
+                )
+                self.note(", ".join(missing), context, offset=OFFSET, code=code)
         elif len(missing) > MAX_ITEMS or len(missing) == len(supertype.type.protocol_members):
             # This is an obviously wrong type: too many missing members
             return
 
         # Report member type conflicts
-        conflict_types = get_conflict_protocol_types(subtype, supertype)
+        conflict_types = get_conflict_protocol_types(subtype, supertype, class_obj=class_obj)
         if conflict_types and (
             not is_subtype(subtype, erase_type(supertype))
             or not subtype.type.defn.type_vars
@@ -1875,16 +1901,30 @@ def report_protocol_problems(
                 else:
                     self.note("Expected:", context, offset=OFFSET, code=code)
                     if isinstance(exp, CallableType):
-                        self.note(pretty_callable(exp), context, offset=2 * OFFSET, code=code)
+                        self.note(
+                            pretty_callable(exp, skip_self=class_obj),
+                            context,
+                            offset=2 * OFFSET,
+                            code=code,
+                        )
                     else:
                         assert isinstance(exp, Overloaded)
-                        self.pretty_overload(exp, context, 2 * OFFSET, code=code)
+                        self.pretty_overload(
+                            exp, context, 2 * OFFSET, code=code, skip_self=class_obj
+                        )
                     self.note("Got:", context, offset=OFFSET, code=code)
                     if isinstance(got, CallableType):
-                        self.note(pretty_callable(got), context, offset=2 * OFFSET, code=code)
+                        self.note(
+                            pretty_callable(got, skip_self=class_obj),
+                            context,
+                            offset=2 * OFFSET,
+                            code=code,
+                        )
                     else:
                         assert isinstance(got, Overloaded)
-                        self.pretty_overload(got, context, 2 * OFFSET, code=code)
+                        self.pretty_overload(
+                            got, context, 2 * OFFSET, code=code, skip_self=class_obj
+                        )
             self.print_more(conflict_types, context, OFFSET, MAX_ITEMS, code=code)
 
         # Report flag conflicts (i.e. settable vs read-only etc.)
@@ -1930,6 +1970,7 @@ def pretty_overload(
         add_class_or_static_decorator: bool = False,
         allow_dups: bool = False,
         code: ErrorCode | None = None,
+        skip_self: bool = False,
     ) -> None:
         for item in tp.items:
             self.note("@overload", context, offset=offset, allow_dups=allow_dups, code=code)
@@ -1940,7 +1981,11 @@ def pretty_overload(
                     self.note(decorator, context, offset=offset, allow_dups=allow_dups, code=code)
 
             self.note(
-                pretty_callable(item), context, offset=offset, allow_dups=allow_dups, code=code
+                pretty_callable(item, skip_self=skip_self),
+                context,
+                offset=offset,
+                allow_dups=allow_dups,
+                code=code,
             )
 
     def print_more(
@@ -2373,10 +2418,14 @@ def pretty_class_or_static_decorator(tp: CallableType) -> str | None:
     return None
 
 
-def pretty_callable(tp: CallableType) -> str:
+def pretty_callable(tp: CallableType, skip_self: bool = False) -> str:
     """Return a nice easily-readable representation of a callable type.
     For example:
         def [T <: int] f(self, x: int, y: T) -> None
+
+    If skip_self is True, print an actual callable type, as it would appear
+    when bound on an instance/class, rather than how it would appear in the
+    defining statement.
     """
     s = ""
     asterisk = False
@@ -2420,7 +2469,11 @@ def [T <: int] f(self, x: int, y: T) -> None
         and hasattr(tp.definition, "arguments")
     ):
         definition_arg_names = [arg.variable.name for arg in tp.definition.arguments]
-        if len(definition_arg_names) > len(tp.arg_names) and definition_arg_names[0]:
+        if (
+            len(definition_arg_names) > len(tp.arg_names)
+            and definition_arg_names[0]
+            and not skip_self
+        ):
             if s:
                 s = ", " + s
             s = definition_arg_names[0] + s
@@ -2487,7 +2540,9 @@ def get_missing_protocol_members(left: Instance, right: Instance) -> list[str]:
     return missing
 
 
-def get_conflict_protocol_types(left: Instance, right: Instance) -> list[tuple[str, Type, Type]]:
+def get_conflict_protocol_types(
+    left: Instance, right: Instance, class_obj: bool = False
+) -> list[tuple[str, Type, Type]]:
     """Find members that are defined in 'left' but have incompatible types.
     Return them as a list of ('member', 'got', 'expected').
     """
@@ -2498,7 +2553,7 @@ def get_conflict_protocol_types(left: Instance, right: Instance) -> list[tuple[s
             continue
         supertype = find_member(member, right, left)
         assert supertype is not None
-        subtype = find_member(member, left, left)
+        subtype = find_member(member, left, left, class_obj=class_obj)
         if not subtype:
             continue
         is_compat = is_subtype(subtype, supertype, ignore_pos_arg_names=True)
diff --git a/mypy/subtypes.py b/mypy/subtypes.py
index 6c0fd6510e7f..1b70a732cefc 100644
--- a/mypy/subtypes.py
+++ b/mypy/subtypes.py
@@ -668,6 +668,14 @@ def visit_callable_type(self, left: CallableType) -> bool:
                 assert call is not None
                 if self._is_subtype(left, call):
                     return True
+            if right.type.is_protocol and left.is_type_obj():
+                ret_type = get_proper_type(left.ret_type)
+                if isinstance(ret_type, TupleType):
+                    ret_type = mypy.typeops.tuple_fallback(ret_type)
+                if isinstance(ret_type, Instance) and is_protocol_implementation(
+                    ret_type, right, proper_subtype=self.proper_subtype, class_obj=True
+                ):
+                    return True
             return self._is_subtype(left.fallback, right)
         elif isinstance(right, TypeType):
             # This is unsound, we don't check the __init__ signature.
@@ -897,6 +905,10 @@ def visit_type_type(self, left: TypeType) -> bool:
             if isinstance(item, TypeVarType):
                 item = get_proper_type(item.upper_bound)
             if isinstance(item, Instance):
+                if right.type.is_protocol and is_protocol_implementation(
+                    item, right, proper_subtype=self.proper_subtype, class_obj=True
+                ):
+                    return True
                 metaclass = item.type.metaclass_type
                 return metaclass is not None and self._is_subtype(metaclass, right)
         return False
@@ -916,7 +928,7 @@ def pop_on_exit(stack: list[tuple[T, T]], left: T, right: T) -> Iterator[None]:
 
 
 def is_protocol_implementation(
-    left: Instance, right: Instance, proper_subtype: bool = False
+    left: Instance, right: Instance, proper_subtype: bool = False, class_obj: bool = False
 ) -> bool:
     """Check whether 'left' implements the protocol 'right'.
 
@@ -959,7 +971,19 @@ def f(self) -> A: ...
             # We always bind self to the subtype. (Similarly to nominal types).
             supertype = get_proper_type(find_member(member, right, left))
             assert supertype is not None
-            subtype = get_proper_type(find_member(member, left, left))
+            if member == "__call__" and class_obj:
+                # Special case: class objects always have __call__ that is just the constructor.
+                # TODO: move this helper function to typeops.py?
+                import mypy.checkmember
+
+                def named_type(fullname: str) -> Instance:
+                    return Instance(left.type.mro[-1], [])
+
+                subtype: ProperType | None = mypy.checkmember.type_object_type(
+                    left.type, named_type
+                )
+            else:
+                subtype = get_proper_type(find_member(member, left, left, class_obj=class_obj))
             # Useful for debugging:
             # print(member, 'of', left, 'has type', subtype)
             # print(member, 'of', right, 'has type', supertype)
@@ -1014,7 +1038,7 @@ def f(self) -> A: ...
 
 
 def find_member(
-    name: str, itype: Instance, subtype: Type, is_operator: bool = False
+    name: str, itype: Instance, subtype: Type, is_operator: bool = False, class_obj: bool = False
 ) -> Type | None:
     """Find the type of member by 'name' in 'itype's TypeInfo.
 
@@ -1027,23 +1051,24 @@ def find_member(
     method = info.get_method(name)
     if method:
         if isinstance(method, Decorator):
-            return find_node_type(method.var, itype, subtype)
+            return find_node_type(method.var, itype, subtype, class_obj=class_obj)
         if method.is_property:
             assert isinstance(method, OverloadedFuncDef)
             dec = method.items[0]
             assert isinstance(dec, Decorator)
-            return find_node_type(dec.var, itype, subtype)
-        return find_node_type(method, itype, subtype)
+            return find_node_type(dec.var, itype, subtype, class_obj=class_obj)
+        return find_node_type(method, itype, subtype, class_obj=class_obj)
     else:
         # don't have such method, maybe variable or decorator?
         node = info.get(name)
         v = node.node if node else None
         if isinstance(v, Var):
-            return find_node_type(v, itype, subtype)
+            return find_node_type(v, itype, subtype, class_obj=class_obj)
         if (
             not v
             and name not in ["__getattr__", "__setattr__", "__getattribute__"]
             and not is_operator
+            and not class_obj
         ):
             for method_name in ("__getattribute__", "__getattr__"):
                 # Normally, mypy assumes that instances that define __getattr__ have all
@@ -1107,7 +1132,9 @@ def get_member_flags(name: str, info: TypeInfo) -> set[int]:
     return set()
 
 
-def find_node_type(node: Var | FuncBase, itype: Instance, subtype: Type) -> Type:
+def find_node_type(
+    node: Var | FuncBase, itype: Instance, subtype: Type, class_obj: bool = False
+) -> Type:
     """Find type of a variable or method 'node' (maybe also a decorated method).
     Apply type arguments from 'itype', and bind 'self' to 'subtype'.
     """
@@ -1129,10 +1156,16 @@ def find_node_type(node: Var | FuncBase, itype: Instance, subtype: Type) -> Type
         and not node.is_staticmethod
     ):
         assert isinstance(p_typ, FunctionLike)
-        signature = bind_self(
-            p_typ, subtype, is_classmethod=isinstance(node, Var) and node.is_classmethod
-        )
-        if node.is_property:
+        if class_obj and not (
+            node.is_class if isinstance(node, FuncBase) else node.is_classmethod
+        ):
+            # Don't bind instance methods on class objects.
+            signature = p_typ
+        else:
+            signature = bind_self(
+                p_typ, subtype, is_classmethod=isinstance(node, Var) and node.is_classmethod
+            )
+        if node.is_property and not class_obj:
             assert isinstance(signature, CallableType)
             typ = signature.ret_type
         else:
diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test
index 36e4959852f7..902a9934cf04 100644
--- a/test-data/unit/check-protocols.test
+++ b/test-data/unit/check-protocols.test
@@ -3138,3 +3138,387 @@ foo(B())  # E: Argument 1 to "foo" has incompatible type "B"; expected "Template
           # N: Following member(s) of "B" have conflicts: \
           # N:     Meta: expected "Type[__main__.Template.Meta]", got "Type[__main__.B.Meta]"
 foo(C())  # OK
+
+[case testProtocolClassObjectAttribute]
+from typing import Protocol
+
+class P(Protocol):
+    foo: int
+
+class B:
+    foo: int
+class C:
+    foo: str
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     foo: expected "int", got "str"
+
+[case testProtocolClassObjectPropertyRejected]
+from typing import Protocol
+
+class P(Protocol):
+    @property
+    def foo(self) -> int: ...
+
+class B:
+    @property
+    def foo(self) -> int: ...
+
+def test(arg: P) -> None: ...
+# TODO: give better diagnostics in this case.
+test(B)  # E: Argument 1 to "test" has incompatible type "Type[B]"; expected "P" \
+         # N: Following member(s) of "B" have conflicts: \
+         # N:     foo: expected "int", got "Callable[[B], int]"
+[builtins fixtures/property.pyi]
+
+[case testProtocolClassObjectInstanceMethod]
+from typing import Any, Protocol
+
+class P(Protocol):
+    def foo(self, obj: Any) -> int: ...
+
+class B:
+    def foo(self) -> int: ...
+class C:
+    def foo(self) -> str: ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo(obj: Any) -> int \
+         # N:     Got: \
+         # N:         def foo(self: C) -> str
+
+[case testProtocolClassObjectInstanceMethodArg]
+from typing import Any, Protocol
+
+class P(Protocol):
+    def foo(self, obj: B) -> int: ...
+
+class B:
+    def foo(self) -> int: ...
+class C:
+    def foo(self) -> int: ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo(obj: B) -> int \
+         # N:     Got: \
+         # N:         def foo(self: C) -> int
+
+[case testProtocolClassObjectInstanceMethodOverloaded]
+from typing import Any, Protocol, overload
+
+class P(Protocol):
+    @overload
+    def foo(self, obj: Any, arg: int) -> int: ...
+    @overload
+    def foo(self, obj: Any, arg: str) -> str: ...
+
+class B:
+    @overload
+    def foo(self, arg: int) -> int: ...
+    @overload
+    def foo(self, arg: str) -> str: ...
+    def foo(self, arg: Any) -> Any:
+        ...
+
+class C:
+    @overload
+    def foo(self, arg: int) -> int: ...
+    @overload
+    def foo(self, arg: str) -> int: ...
+    def foo(self, arg: Any) -> Any:
+        ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         @overload \
+         # N:         def foo(obj: Any, arg: int) -> int \
+         # N:         @overload \
+         # N:         def foo(obj: Any, arg: str) -> str \
+         # N:     Got: \
+         # N:         @overload \
+         # N:         def foo(self: C, arg: int) -> int \
+         # N:         @overload \
+         # N:         def foo(self: C, arg: str) -> int
+
+[case testProtocolClassObjectClassMethod]
+from typing import Protocol
+
+class P(Protocol):
+    def foo(self) -> int: ...
+
+class B:
+    @classmethod
+    def foo(cls) -> int: ...
+class C:
+    @classmethod
+    def foo(cls) -> str: ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo() -> int \
+         # N:     Got: \
+         # N:         def foo() -> str
+[builtins fixtures/classmethod.pyi]
+
+[case testProtocolClassObjectStaticMethod]
+from typing import Protocol
+
+class P(Protocol):
+    def foo(self) -> int: ...
+
+class B:
+    @staticmethod
+    def foo() -> int: ...
+class C:
+    @staticmethod
+    def foo() -> str: ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo() -> int \
+         # N:     Got: \
+         # N:         def foo() -> str
+[builtins fixtures/staticmethod.pyi]
+
+[case testProtocolClassObjectGenericAttribute]
+from typing import Protocol, Generic, List, TypeVar
+
+class P(Protocol):
+    foo: List[int]
+
+T = TypeVar("T")
+class A(Generic[T]):
+    foo: T
+class AA(A[List[T]]): ...
+
+class B(AA[int]): ...
+class C(AA[str]): ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     foo: expected "List[int]", got "List[str]"
+[builtins fixtures/list.pyi]
+
+[case testProtocolClassObjectGenericInstanceMethod]
+from typing import Any, Protocol, Generic, List, TypeVar
+
+class P(Protocol):
+    def foo(self, obj: Any) -> List[int]: ...
+
+T = TypeVar("T")
+class A(Generic[T]):
+    def foo(self) -> T: ...
+class AA(A[List[T]]): ...
+
+class B(AA[int]): ...
+class C(AA[str]): ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo(obj: Any) -> List[int] \
+         # N:     Got: \
+         # N:         def foo(self: A[List[str]]) -> List[str]
+[builtins fixtures/list.pyi]
+
+[case testProtocolClassObjectGenericClassMethod]
+from typing import Any, Protocol, Generic, List, TypeVar
+
+class P(Protocol):
+    def foo(self) -> List[int]: ...
+
+T = TypeVar("T")
+class A(Generic[T]):
+    @classmethod
+    def foo(self) -> T: ...
+class AA(A[List[T]]): ...
+
+class B(AA[int]): ...
+class C(AA[str]): ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo() -> List[int] \
+         # N:     Got: \
+         # N:         def foo() -> List[str]
+[builtins fixtures/isinstancelist.pyi]
+
+[case testProtocolClassObjectSelfTypeInstanceMethod]
+from typing import Protocol, TypeVar, Union
+
+T = TypeVar("T")
+class P(Protocol):
+    def foo(self, arg: T) -> T: ...
+
+class B:
+    def foo(self: T) -> T: ...
+class C:
+    def foo(self: T) -> Union[T, int]: ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def [T] foo(arg: T) -> T \
+         # N:     Got: \
+         # N:         def [T] foo(self: T) -> Union[T, int]
+
+[case testProtocolClassObjectSelfTypeClassMethod]
+from typing import Protocol, Type, TypeVar
+
+T = TypeVar("T")
+class P(Protocol):
+    def foo(self) -> B: ...
+
+class B:
+    @classmethod
+    def foo(cls: Type[T]) -> T: ...
+class C:
+    @classmethod
+    def foo(cls: Type[T]) -> T: ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo() -> B \
+         # N:     Got: \
+         # N:         def foo() -> C
+[builtins fixtures/classmethod.pyi]
+
+[case testProtocolClassObjectAttributeAndCall]
+from typing import Any, Protocol
+
+class P(Protocol):
+    foo: int
+    def __call__(self, x: int, y: int) -> Any: ...
+
+class B:
+    foo: int
+    def __init__(self, x: int, y: int) -> None: ...
+class C:
+    foo: int
+    def __init__(self, x: int, y: str) -> None: ...
+
+def test(arg: P) -> None: ...
+test(B)  # OK
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: "C" has constructor incompatible with "__call__" of "P"
+
+[case testProtocolTypeTypeAttribute]
+from typing import Protocol, Type
+
+class P(Protocol):
+    foo: int
+
+class B:
+    foo: int
+class C:
+    foo: str
+
+def test(arg: P) -> None: ...
+b: Type[B]
+c: Type[C]
+test(b)  # OK
+test(c)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     foo: expected "int", got "str"
+
+[case testProtocolTypeTypeInstanceMethod]
+from typing import Any, Protocol, Type
+
+class P(Protocol):
+    def foo(self, cls: Any) -> int: ...
+
+class B:
+    def foo(self) -> int: ...
+class C:
+    def foo(self) -> str: ...
+
+def test(arg: P) -> None: ...
+b: Type[B]
+c: Type[C]
+test(b)  # OK
+test(c)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo(cls: Any) -> int \
+         # N:     Got: \
+         # N:         def foo(self: C) -> str
+
+[case testProtocolTypeTypeClassMethod]
+from typing import Protocol, Type
+
+class P(Protocol):
+    def foo(self) -> int: ...
+
+class B:
+    @classmethod
+    def foo(cls) -> int: ...
+class C:
+    @classmethod
+    def foo(cls) -> str: ...
+
+def test(arg: P) -> None: ...
+b: Type[B]
+c: Type[C]
+test(b)  # OK
+test(c)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def foo() -> int \
+         # N:     Got: \
+         # N:         def foo() -> str
+[builtins fixtures/classmethod.pyi]
+
+[case testProtocolTypeTypeSelfTypeInstanceMethod]
+from typing import Protocol, Type, TypeVar, Union
+
+T = TypeVar("T")
+class P(Protocol):
+    def foo(self, arg: T) -> T: ...
+
+class B:
+    def foo(self: T) -> T: ...
+class C:
+    def foo(self: T) -> Union[T, int]: ...
+
+def test(arg: P) -> None: ...
+b: Type[B]
+c: Type[C]
+test(b)  # OK
+test(c)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Following member(s) of "C" have conflicts: \
+         # N:     Expected: \
+         # N:         def [T] foo(arg: T) -> T \
+         # N:     Got: \
+         # N:         def [T] foo(self: T) -> Union[T, int]


Code review comments:
diff_hunk: @@ -3138,3 +3138,387 @@ foo(B())  # E: Argument 1 to "foo" has incompatible type "B"; expected "Template
           # N: Following member(s) of "B" have conflicts: \
           # N:     Meta: expected "Type[__main__.Template.Meta]", got "Type[__main__.B.Meta]"
 foo(C())  # OK
+
+[case testProtocolClassObjectAttribute]
+from typing import Protocol
+
+class P(Protocol):
+    foo: int
+
+class B:
+    foo: int
+class C:
+    foo: str
+
+def test(arg: P) -> None: ...
+test(B)  # OK
path: test-data/unit/check-protocols.test
text: @user1:
Should this require ClassVar?

@author:
Hm, good question. I think yes. And we can also allow the not annotated (IIRC those are implicitly class variables per PEP 526). Let me update the PR.