Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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`
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
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
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
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