Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
521a6a5
Deprecate inherited runtime checkability of protocols
johnslavik Jan 13, 2026
0adbbff
Add tests
johnslavik Jan 13, 2026
985e267
Document runtime-checkability on protocols
johnslavik Jan 13, 2026
a583cce
Add news entry
johnslavik Jan 13, 2026
5be278f
Move `RCProto3` below `RCProto2`
johnslavik Jan 13, 2026
8886908
Simplify docstring of test class `BaseProto`
johnslavik Jan 13, 2026
62dcdfe
Simplify non-RC proto refs in test class docstrings
johnslavik Jan 13, 2026
cef6504
Fix grammar error
johnslavik Jan 13, 2026
484381c
Add some methods to protocols
johnslavik Jan 13, 2026
bc1cbcf
Force re-run of doc preview build
johnslavik Jan 13, 2026
84d3af6
Use arguments from subtests parametrization!
johnslavik Jan 13, 2026
ec78805
Merge branch 'main' into deprecate-inherited-runtime-protocols
johnslavik Jan 13, 2026
4807a81
Fix semantic typo in tests
johnslavik Jan 13, 2026
c2ff476
Add helpful comment about `_is_deprecated_inherited_runtime_protocol`
johnslavik Jan 16, 2026
a312da9
Clarify and reposition the helpful comment
johnslavik Jan 18, 2026
1736d55
Apply suggestions from code review
johnslavik Jan 19, 2026
406aff6
Change flag name to `__typing_is_deprecated_inherited_runtime_protoco…
johnslavik Jan 19, 2026
3a3067a
Reword news entry
johnslavik Jan 19, 2026
62082cd
Update test message re to match
johnslavik Jan 19, 2026
5bccce1
Fix tenses in news entry
johnslavik Jan 19, 2026
eda3442
Merge branch 'main' into deprecate-inherited-runtime-protocols
johnslavik Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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::
Expand Down Expand Up @@ -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)

Expand Down
70 changes: 67 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__',
})

Expand Down Expand Up @@ -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__
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading