diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index eaa0ba54af18e7..3bbfddc49955cb 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -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. @@ -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 ` 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) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index e896df518447c5..f9c93aeb91980f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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', '_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" 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 diff --git a/Lib/typing.py b/Lib/typing.py index 1a2ef8c086f772..cf366a76b4141d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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({ @@ -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__ @@ -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 @@ -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 @@ -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, diff --git a/Misc/NEWS.d/next/Library/2026-01-13-15-56-03.gh-issue-132604.lvjNTr.rst b/Misc/NEWS.d/next/Library/2026-01-13-15-56-03.gh-issue-132604.lvjNTr.rst new file mode 100644 index 00000000000000..81c85b267c6e2c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-13-15-56-03.gh-issue-132604.lvjNTr.rst @@ -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.