Repository: python/cpython
Pull request: gh-112345: Let failed protocol subclasscheck show non-method members
See https://github.com/python/cpython/pull/112340#discussion_r1403566211

Closes #112345

<!-- gh-issue-number: gh-112345 -->
* Issue: gh-112345
<!-- /gh-issue-number -->


Problem statement:
Improve error message when trying to call `issubclass()` against a Protocol that has non-method members
# Feature or enhancement

### Proposal:

The error message could tell the user what the non-method members in the protocol are.

### Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

### Links to previous discussion of this feature:

_No response_

<!-- gh-linked-prs -->
### Linked PRs
* gh-112344
<!-- /gh-linked-prs -->

Hints from issue discussion:

Commit message: added test

Patch to review:
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 2b5f34b4b92e0c..3c819516dff179 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -4091,6 +4091,25 @@ def method(self) -> None: ...
         self.assertIsInstance(Foo(), ProtocolWithMixedMembers)
         self.assertNotIsInstance(42, ProtocolWithMixedMembers)
 
+    def test_protocol_issubclass_error_message(self):
+        class Vec2D(Protocol):
+            x: float
+            y: float
+
+            def square_norm(self) -> float:
+                return self.x ** 2 + self.y ** 2
+
+        assert Vec2D.__protocol_attrs__ == {'x', 'y', 'square_norm'}
+        expected_error_message = (
+            "Protocols with non-method members don't support issubclass()."
+            " Non-method members: {'x', 'y'}."
+        )
+
+        try:
+            issubclass(int, Vec2D)
+        except TypeError as exc:
+            self.assertEqual(str(exc), expected_error_message)
+
 
 class GenericTests(BaseTestCase):
 
diff --git a/Lib/typing.py b/Lib/typing.py
index a96c7083eb785e..4affc1cadacf3d 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1828,8 +1828,13 @@ def __subclasscheck__(cls, other):
                 not cls.__callable_proto_members_only__
                 and cls.__dict__.get("__subclasshook__") is _proto_hook
             ):
+                non_method_attrs = {
+                    attr for attr in cls.__protocol_attrs__
+                    if not callable(getattr(cls, attr, None))
+                }
                 raise TypeError(
-                    "Protocols with non-method members don't support issubclass()"
+                    "Protocols with non-method members don't support issubclass()."
+                    f" Non-method members: {non_method_attrs}."
                 )
             if not getattr(cls, '_is_runtime_protocol', False):
                 raise TypeError(
diff --git a/Misc/NEWS.d/next/Library/2023-11-23-17-25-27.gh-issue-112345.FFApHx.rst b/Misc/NEWS.d/next/Library/2023-11-23-17-25-27.gh-issue-112345.FFApHx.rst
new file mode 100644
index 00000000000000..511fe47fdc0c0b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-11-23-17-25-27.gh-issue-112345.FFApHx.rst
@@ -0,0 +1,2 @@
+Improve error message when trying to call :func:`issubclass` against a :class:`typing.Protocol` that has non-method members.
+Patch by Randolf Scholz.


Code review comments:
diff_hunk: @@ -4091,6 +4091,25 @@ def method(self) -> None: ...
         self.assertIsInstance(Foo(), ProtocolWithMixedMembers)
         self.assertNotIsInstance(42, ProtocolWithMixedMembers)
 
+    def test_protocol_issubclass_error_message(self):
+        class Vec2D(Protocol):
+            x: float
+            y: float
+
+            def square_norm(self) -> float:
+                return self.x ** 2 + self.y ** 2
+
+        assert Vec2D.__protocol_attrs__ == {'x', 'y', 'square_norm'}
+        expected_error_message = (
+            "Protocols with non-method members don't support issubclass()."
+            " Non-method members: {'x', 'y'}."
+        )
+
+        try:
+            issubclass(int, Vec2D)
+        except TypeError as exc:
+            self.assertEqual(str(exc), expected_error_message)
path: Lib/test/test_typing.py
text: @user1:
Use `assertRaisesRegex` (there's other examples of it being used in this file if you're new to it)

diff_hunk: @@ -4091,6 +4091,25 @@ def method(self) -> None: ...
         self.assertIsInstance(Foo(), ProtocolWithMixedMembers)
         self.assertNotIsInstance(42, ProtocolWithMixedMembers)
 
+    def test_protocol_issubclass_error_message(self):
+        class Vec2D(Protocol):
+            x: float
+            y: float
+
+            def square_norm(self) -> float:
+                return self.x ** 2 + self.y ** 2
+
+        assert Vec2D.__protocol_attrs__ == {'x', 'y', 'square_norm'}
path: Lib/test/test_typing.py
text: @user1:
Never use bare `assert`s in tests run with unittest

```suggestion
        self.assertEqual(Vec2D.__protocol_attrs__, {'x', 'y', 'square_norm'})
```