.. 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.
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::
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)
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,
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)
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
_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__',
})
"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__
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
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
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,
--- /dev/null
+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.