From e0c8e43b82b5f2fe814b44d2be9691644d520499 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Mon, 9 Mar 2026 23:00:15 -0700 Subject: [PATCH 1/3] gh-145688: Fix _get_protocol_attrs matching user classes named "Protocol" or "Generic" The `_get_protocol_attrs` function used `base.__name__ in {'Protocol', 'Generic'}` to skip the base typing classes, which also skipped user-defined Protocol subclasses that happened to be named "Protocol" or "Generic". This caused `get_protocol_members()` and `__protocol_attrs__` to return empty results for such classes. Changed to check both `__name__` and `__module__` to ensure only the actual `typing.Protocol` and `typing.Generic` classes are skipped. Co-Authored-By: Claude Opus 4.6 --- Lib/test/test_typing.py | 13 +++++++++++++ Lib/typing.py | 2 +- ...03-09-00-00-00.gh-issue-145688.protocol-name.rst | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index c6f08ff8a052ab..e7bf94ee8cc1e8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4574,6 +4574,19 @@ def c(self) -> int: return 5 with self.assertRaisesRegex(TypeError, "not a Protocol"): get_protocol_members(ConcreteInherit()) + def test_get_protocol_members_named_protocol_or_generic(self): + # gh-145688: Protocols named "Protocol" or "Generic" should still + # have their members collected correctly. + class Protocol(typing.Protocol): + a: int + + self.assertEqual(get_protocol_members(Protocol), {'a'}) + + class Generic(typing.Protocol): + b: str + + self.assertEqual(get_protocol_members(Generic), {'b'}) + def test_is_protocol(self): self.assertTrue(is_protocol(Proto)) self.assertTrue(is_protocol(Point)) diff --git a/Lib/typing.py b/Lib/typing.py index e78fb8b71a996c..c4110483214fc1 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1884,7 +1884,7 @@ def _get_protocol_attrs(cls): """ attrs = set() for base in cls.__mro__[:-1]: # without object - if base.__name__ in {'Protocol', 'Generic'}: + if base.__name__ in {'Protocol', 'Generic'} and base.__module__ == 'typing': continue try: annotations = base.__annotations__ diff --git a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst new file mode 100644 index 00000000000000..8cc0f989a971e6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst @@ -0,0 +1,5 @@ +Fixed :func:`typing.get_protocol_members` and :attr:`~typing.Protocol.__protocol_attrs__` +returning empty results for Protocol subclasses named ``"Protocol"`` or ``"Generic"``. +The check in :func:`_get_protocol_attrs` now uses identity comparison instead of +name-based comparison to skip the base :class:`typing.Protocol` and +:class:`typing.Generic` classes. From 42c37503c2a1868e27f0e4280c5da3196ba4d549 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Tue, 10 Mar 2026 07:05:55 -0700 Subject: [PATCH 2/3] gh-145688: Handle typing_extensions.Protocol in _get_protocol_attrs Address review feedback: also skip bases from typing_extensions module so the backported Protocol class is treated the same as typing.Protocol. Simplify NEWS entry to focus on user-visible behavior. Co-Authored-By: Claude Opus 4.6 --- Lib/typing.py | 2 +- .../2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index c4110483214fc1..52683240d42051 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1884,7 +1884,7 @@ def _get_protocol_attrs(cls): """ attrs = set() for base in cls.__mro__[:-1]: # without object - if base.__name__ in {'Protocol', 'Generic'} and base.__module__ == 'typing': + if base.__name__ in {'Protocol', 'Generic'} and base.__module__ in {'typing', 'typing_extensions'}: continue try: annotations = base.__annotations__ diff --git a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst index 8cc0f989a971e6..27839f9c52976e 100644 --- a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst +++ b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145688.protocol-name.rst @@ -1,5 +1,2 @@ -Fixed :func:`typing.get_protocol_members` and :attr:`~typing.Protocol.__protocol_attrs__` -returning empty results for Protocol subclasses named ``"Protocol"`` or ``"Generic"``. -The check in :func:`_get_protocol_attrs` now uses identity comparison instead of -name-based comparison to skip the base :class:`typing.Protocol` and -:class:`typing.Generic` classes. +Fixed :func:`typing.get_protocol_members` returning empty results for +:class:`typing.Protocol` subclasses named ``"Protocol"`` or ``"Generic"``. From ff6435a97ef91cf2d4a633aeadd43e01eb954abb Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 10 Mar 2026 08:26:23 -0700 Subject: [PATCH 3/3] gh-145688: Use identity check instead of name+module allowlist Per @AlexWaygood's review, checking base.__module__ in {'typing', 'typing_extensions'} is fragile and would break third-party libraries like beartype that define their own Protocol using _ProtocolMeta. Use `base is Protocol or base is Generic` instead, which is the same pattern already used at line 1176 in _generic_class_getitem. This precisely targets the two base classes we want to skip without affecting any third-party Protocol implementations. Co-Authored-By: Claude Opus 4.6 --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 52683240d42051..d1fbc893b37d7c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1884,7 +1884,7 @@ def _get_protocol_attrs(cls): """ attrs = set() for base in cls.__mro__[:-1]: # without object - if base.__name__ in {'Protocol', 'Generic'} and base.__module__ in {'typing', 'typing_extensions'}: + if base is Protocol or base is Generic: continue try: annotations = base.__annotations__