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

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
Bartosz Sławecki 2026-02-09 15:13:47 +01:00 committed by GitHub
parent a91b5c3fb5
commit ff531f9005
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 125 additions and 3 deletions

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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.