Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-132604: Deprecate inherited runtime checkability of protocols #143806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
johnslavik wants to merge 13 commits into python:main
base: main
Choose a base branch
Loading
from johnslavik:deprecate-inherited-runtime-protocols
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 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
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
View file Open in desktop
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`
after subclassing runtime-checkable protocol classes. 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`
after subclassing runtime-checkable protocol classes. 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
View file Open in desktop
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', '_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 may stop working 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
33 changes: 31 additions & 2 deletions Lib/typing.py
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1825,8 +1825,8 @@ class _TypingEllipsis:

_TYPING_INTERNALS = frozenset({
'__parameters__', '__orig_bases__', '__orig_class__',
'_is_protocol', '_is_runtime_protocol', '__protocol_attrs__',
'__non_callable_proto_members__', '__type_params__',
'_is_protocol', '_is_runtime_protocol', '_is_deprecated_inherited_runtime_protocol',
'__protocol_attrs__', '__non_callable_proto_members__', '__type_params__',
})

_SPECIAL_NAMES = frozenset({
Expand Down Expand Up @@ -2015,6 +2015,16 @@ def __subclasscheck__(cls, other):
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
)
if getattr(cls, '_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 may stop working 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 +2054,18 @@ def __instancecheck__(cls, instance):
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")

if getattr(cls, '_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 may stop working 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 +2158,10 @@ 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):
cls._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 +2308,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, '_is_deprecated_inherited_runtime_protocol'):
cls._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
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
It is deprecated to call :func:`isinstance` and :func:`issubclass` checks on
:class:`typing.Protocol` classes that were not explicitly decorated
with :func:`typing.runtime_checkable` after subclassing runtime-checkable
protocol classes. This will throw a :exc:`TypeError` in Python 3.20.
Contributed by Bartosz Sławecki.
Loading

AltStyle によって変換されたページ (->オリジナル) /