https://github.com/python/cpython/commit/ff531f9005d508cbac31ffdd696ef45bc4944d61
commit: ff531f9005d508cbac31ffdd696ef45bc4944d61
branch: main
author: Bartosz SÅ‚awecki <[email protected]>
committer: encukou <[email protected]>
date: 2026-02-09T15:13:47+01:00
summary:

gh-132604: Deprecate inherited runtime checkability of protocols (GH-143806)


Co-authored-by: Jelle Zijlstra <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-01-13-15-56-03.gh-issue-132604.lvjNTr.rst
M Doc/library/typing.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index eaa0ba54af18e7..7b62b9208412e9 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -2527,6 +2527,12 @@ types.
 
    .. versionadded:: 3.8
 
+   .. deprecated-removed:: 3.15 3.20
+      It is deprecated to call :func:`isinstance` and :func:`issubclass` 
checks on
+      protocol classes that were not explicitly decorated with 
:func:`!runtime_checkable`
+      but that inherit from a runtime-checkable protocol class. This will throw
+      a :exc:`TypeError` in Python 3.20.
+
 .. decorator:: runtime_checkable
 
    Mark a protocol class as a runtime protocol.
@@ -2548,6 +2554,18 @@ types.
       import threading
       assert isinstance(threading.Thread(name='Bob'), Named)
 
+   Runtime checkability of protocols is not inherited. A subclass of a 
runtime-checkable protocol
+   is only runtime-checkable if it is explicitly marked as such, regardless of 
class hierarchy::
+
+      @runtime_checkable
+      class Iterable(Protocol):
+          def __iter__(self): ...
+
+      # Without @runtime_checkable, Reversible would no longer be 
runtime-checkable.
+      @runtime_checkable
+      class Reversible(Iterable, Protocol):
+          def __reversed__(self): ...
+
    This decorator raises :exc:`TypeError` when applied to a non-protocol class.
 
    .. note::
@@ -2588,6 +2606,11 @@ types.
       protocol. See :ref:`What's new in Python 3.12 <whatsnew-typing-py312>`
       for more details.
 
+   .. deprecated-removed:: 3.15 3.20
+      It is deprecated to call :func:`isinstance` and :func:`issubclass` 
checks on
+      protocol classes that were not explicitly decorated with 
:func:`!runtime_checkable`
+      but that inherit from a runtime-checkable protocol class. This will throw
+      a :exc:`TypeError` in Python 3.20.
 
 .. class:: TypedDict(dict)
 
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index e896df518447c5..72ae7776ab9062 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -51,7 +51,7 @@
 
 from test.support import (
     captured_stderr, cpython_only, requires_docstrings, import_helper, 
run_code,
-    EqualToForwardRef,
+    subTests, EqualToForwardRef,
 )
 from test.typinganndata import (
     ann_module695, mod_generics_cache, _typed_dict_helper,
@@ -3885,8 +3885,8 @@ def meth(self): pass
         self.assertIsNot(get_protocol_members(PR), P.__protocol_attrs__)
 
         acceptable_extra_attrs = {
-            '_is_protocol', '_is_runtime_protocol', '__parameters__',
-            '__init__', '__annotations__', '__subclasshook__', '__annotate__',
+            '_is_protocol', '_is_runtime_protocol', 
'__typing_is_deprecated_inherited_runtime_protocol__',
+            '__parameters__', '__init__', '__annotations__', 
'__subclasshook__', '__annotate__',
             '__annotations_cache__', '__annotate_func__',
         }
         self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | 
acceptable_extra_attrs)
@@ -4458,6 +4458,70 @@ class P(Protocol):
         with self.assertRaisesRegex(TypeError, "@runtime_checkable"):
             isinstance(1, P)
 
+    @subTests(['check_obj', 'check_func'], ([42, isinstance], [frozenset, 
issubclass]))
+    def test_inherited_runtime_protocol_deprecated(self, check_obj, 
check_func):
+        """See GH-132604."""
+
+        class BareProto(Protocol):
+            """I am not runtime-checkable."""
+
+        @runtime_checkable
+        class RCProto1(Protocol):
+            """I am runtime-checkable."""
+
+        class InheritedRCProto1(RCProto1, Protocol):
+            """I am accidentally runtime-checkable (by inheritance)."""
+
+        @runtime_checkable
+        class RCProto2(InheritedRCProto1, Protocol):
+            """Explicit RC -> inherited RC -> explicit RC."""
+            def spam(self): ...
+
+        @runtime_checkable
+        class RCProto3(BareProto, Protocol):
+            """Not RC -> explicit RC."""
+
+        class InheritedRCProto2(RCProto3, Protocol):
+            """Not RC -> explicit RC -> inherited RC."""
+            def eggs(self): ...
+
+        class InheritedRCProto3(RCProto2, Protocol):
+            """Explicit RC -> inherited RC -> explicit RC -> inherited RC."""
+
+        class Concrete1(BareProto):
+            pass
+
+        class Concrete2(InheritedRCProto2):
+            pass
+
+        class Concrete3(InheritedRCProto3):
+            pass
+
+        depr_message_re = (
+            r"<class .+\.InheritedRCProto\d'> isn't explicitly decorated "
+            r"with @runtime_checkable but it is used in issubclass\(\) or "
+            r"isinstance\(\). Instance and class checks can only be used with "
+            r"@runtime_checkable protocols. This will raise a TypeError in 
Python 3.20."
+        )
+
+        for inherited_runtime_proto in InheritedRCProto1, InheritedRCProto2, 
InheritedRCProto3:
+            with self.assertWarnsRegex(DeprecationWarning, depr_message_re):
+                check_func(check_obj, inherited_runtime_proto)
+
+        # Don't warn for explicitly checkable protocols and concrete 
implementations.
+        with warnings.catch_warnings():
+            warnings.simplefilter("error", DeprecationWarning)
+
+            for checkable in RCProto1, RCProto2, RCProto3, Concrete1, 
Concrete2, Concrete3:
+                check_func(check_obj, checkable)
+
+        # Don't warn for uncheckable protocols.
+        with warnings.catch_warnings():
+            warnings.simplefilter("error", DeprecationWarning)
+
+            with self.assertRaises(TypeError):  # Self-test. Protocol below 
can't be runtime-checkable.
+                check_func(check_obj, BareProto)
+
     def test_super_call_init(self):
         class P(Protocol):
             x: int
diff --git a/Lib/typing.py b/Lib/typing.py
index 1a2ef8c086f772..71a08a5f1df811 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1826,6 +1826,7 @@ class _TypingEllipsis:
 _TYPING_INTERNALS = frozenset({
     '__parameters__', '__orig_bases__',  '__orig_class__',
     '_is_protocol', '_is_runtime_protocol', '__protocol_attrs__',
+    '__typing_is_deprecated_inherited_runtime_protocol__',
     '__non_callable_proto_members__', '__type_params__',
 })
 
@@ -2015,6 +2016,16 @@ def __subclasscheck__(cls, other):
                     "Instance and class checks can only be used with "
                     "@runtime_checkable protocols"
                 )
+            if getattr(cls, 
'__typing_is_deprecated_inherited_runtime_protocol__', False):
+                # See GH-132604.
+                import warnings
+                depr_message = (
+                    f"{cls!r} isn't explicitly decorated with 
@runtime_checkable but "
+                    "it is used in issubclass() or isinstance(). Instance and 
class "
+                    "checks can only be used with @runtime_checkable 
protocols. "
+                    "This will raise a TypeError in Python 3.20."
+                )
+                warnings.warn(depr_message, category=DeprecationWarning, 
stacklevel=2)
             if (
                 # this attribute is set by @runtime_checkable:
                 cls.__non_callable_proto_members__
@@ -2044,6 +2055,18 @@ def __instancecheck__(cls, instance):
             raise TypeError("Instance and class checks can only be used with"
                             " @runtime_checkable protocols")
 
+        if getattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__', 
False):
+            # See GH-132604.
+            import warnings
+
+            depr_message = (
+                f"{cls!r} isn't explicitly decorated with @runtime_checkable 
but "
+                "it is used in issubclass() or isinstance(). Instance and 
class "
+                "checks can only be used with @runtime_checkable protocols. "
+                "This will raise a TypeError in Python 3.20."
+            )
+            warnings.warn(depr_message, category=DeprecationWarning, 
stacklevel=2)
+
         if _abc_instancecheck(cls, instance):
             return True
 
@@ -2136,6 +2159,11 @@ def __init_subclass__(cls, *args, **kwargs):
         if not cls.__dict__.get('_is_protocol', False):
             cls._is_protocol = any(b is Protocol for b in cls.__bases__)
 
+        # Mark inherited runtime checkability (deprecated). See GH-132604.
+        if cls._is_protocol and getattr(cls, '_is_runtime_protocol', False):
+            # This flag is set to False by @runtime_checkable.
+            cls.__typing_is_deprecated_inherited_runtime_protocol__ = True
+
         # Set (or override) the protocol subclass hook.
         if '__subclasshook__' not in cls.__dict__:
             cls.__subclasshook__ = _proto_hook
@@ -2282,6 +2310,9 @@ def close(self): ...
         raise TypeError('@runtime_checkable can be only applied to protocol 
classes,'
                         ' got %r' % cls)
     cls._is_runtime_protocol = True
+    # See GH-132604.
+    if hasattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__'):
+        cls.__typing_is_deprecated_inherited_runtime_protocol__ = False
     # PEP 544 prohibits using issubclass()
     # with protocols that have non-method members.
     # See gh-113320 for why we compute this attribute here,
diff --git 
a/Misc/NEWS.d/next/Library/2026-01-13-15-56-03.gh-issue-132604.lvjNTr.rst 
b/Misc/NEWS.d/next/Library/2026-01-13-15-56-03.gh-issue-132604.lvjNTr.rst
new file mode 100644
index 00000000000000..92c4dbb536cdf6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-01-13-15-56-03.gh-issue-132604.lvjNTr.rst
@@ -0,0 +1,4 @@
+Previously, :class:`~typing.Protocol` classes that were not decorated with 
:deco:`~typing.runtime_checkable`,
+but that inherited from another ``Protocol`` class that did have this 
decorator, could be used in :func:`isinstance`
+and :func:`issubclass` checks. This behavior is now deprecated and such checks 
will throw a :exc:`TypeError`
+in Python 3.20. Patch by Bartosz Sławecki.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to