gh-132604: Deprecate inherited runtime checkability of protocols (GH-143806)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index eaa0ba5..7b62b92 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -2527,6 +2527,12 @@
.. 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 @@
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 @@
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 e896df5..72ae777 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 1a2ef8c..71a08a5 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 0000000..92c4dbb
--- /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.