Description
Python 3.11 introduced an undocumented behaviour change for protocols decorated with both @final
and @runtime_checkable
. On 3.10:
>>> from typing import *
>>> @final
... @runtime_checkable
... class Foo(Protocol):
... def bar(self): ...
...
>>> class Spam:
... def bar(self): ...
...
>>> issubclass(Spam, Foo)
True
>>> isinstance(Spam(), Foo)
True
On 3.11:
>>> from typing import *
>>> @final
... @runtime_checkable
... class Foo(Protocol):
... def bar(self): ...
...
>>> class Spam:
... def bar(self): ...
...
>>> issubclass(Spam, Foo)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen abc>", line 123, in __subclasscheck__
File "C:\Users\alexw\coding\cpython\Lib\typing.py", line 1547, in _proto_hook
raise TypeError("Protocols with non-method members"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: Protocols with non-method members don't support issubclass()
>>> isinstance(Spam(), Foo)
False
This is because, following 0bbf30e (by @JelleZijlstra), the @final
decorator sets a __final__
attribute wherever it can, so that it is introspectable by runtime tools. But the runtime-checkable-protocol isinstance()
machinery doesn't know anything about the __final__
attribute, so it assumes that the __final__
attribute is just a regular protocol member.
This should be pretty easy to fix: we just need to add __final__
to the set of "special attributes" ignored by runtime-checkable-protocol isinstance()
/issubclass()
checks here:
Lines 1906 to 1909 in 848bdbe
@JelleZijlstra do you agree with that course of action?
Linked PRs
- gh-103171: Document and test behaviour change in 3.11 for runtime-checkable protocols decorated with
@final
#103173 - [3.11] gh-103171: Revert undocumented behaviour change for runtime-checkable protocols decorated with
@final
#105445 - gh-103171: Forward-port new tests for runtime-checkable protocols decorated with
@final
#105473 - [3.12] gh-103171: Forward-port new tests for runtime-checkable protocols decorated with
@final
(GH-105473) #105474