(In very specific circumstances)
Bug report
Due to some changes that have been made to the implementation of typing.Protocol in Python 3.12, whether or not isinstance([], collections.abc.Mapping) evaluates to True or False now depends on precisely when and if garbage collection happens.
Minimal repro:
>>> import gc, collections.abc, typing
>>> gc.disable()
>>> try:
... class Foo(collections.abc.Mapping, typing.Protocol): pass
... except TypeError:
... pass
...
>>> isinstance([], collections.abc.Mapping)
True
Why does this happen?! It's because Foo is an illegal class, so TypeError is raised during class initialisation here:
|
# ... otherwise check consistency of bases, and prohibit instantiation. |
|
for base in cls.__bases__: |
|
if not (base in (object, Generic) or |
|
base.__module__ in _PROTO_ALLOWLIST and |
|
base.__name__ in _PROTO_ALLOWLIST[base.__module__] or |
|
issubclass(base, Generic) and getattr(base, '_is_protocol', False)): |
|
raise TypeError('Protocols can only inherit from other' |
|
' protocols, got %r' % base) |
TypeError being raised here means that Foo never has __protocol_attrs__ set on it. But, the error is raised after class construction, meaning that if garbage collection happens at the wrong time, a reference to Foo is still retained in collections.abc.Mapping.__subclasses__(). This then creates problems in the isinstance() check, because of some behaviour in the abc module where it iterates through all of the subclasses of collections.abc.Mapping in order to determine whether an object is an instance of collections.abc.Mapping.
>>> import gc, collections.abc, typing
>>> gc.disable()
>>> try:
... class Foo(collections.abc.Mapping, typing.Protocol): pass
... except TypeError:
... pass
...
>>> x = collections.abc.Mapping.__subclasses__()
>>> x
[<class 'collections.abc.MutableMapping'>, <class '__main__.Foo'>]
>>> x[-1].__protocol_attrs__
set()
The fix is to raise TypeError before the class has actually been created, rather than during class initialisation.
Why does this matter?
This might seem like an absurdly specific bug report, but it would be good to see it fixed. It was causing bizarre test failures in the typing_extensions backport. The issue was that we did this in one test method:
def test_protocols_bad_subscripts(self):
T = TypeVar('T')
S = TypeVar('S')
with self.assertRaises(TypeError):
class P(typing.Mapping[T, S], Protocol[T]): pass
...And that final P class wasn't being cleaned up by the garbage collector immediately after that test having been run. That then caused an unrelated test elsewhere to fail due to the fact that isinstance([], collections.abc.Mapping) was evaluating to True, and it was very hard to figure out why.
Linked PRs
(In very specific circumstances)
Bug report
Due to some changes that have been made to the implementation of
typing.Protocolin Python 3.12, whether or notisinstance([], collections.abc.Mapping)evaluates toTrueorFalsenow depends on precisely when and if garbage collection happens.Minimal repro:
Why does this happen?! It's because
Foois an illegal class, soTypeErroris raised during class initialisation here:cpython/Lib/typing.py
Lines 1905 to 1912 in ce558e6
TypeErrorbeing raised here means thatFoonever has__protocol_attrs__set on it. But, the error is raised after class construction, meaning that if garbage collection happens at the wrong time, a reference toFoois still retained incollections.abc.Mapping.__subclasses__(). This then creates problems in theisinstance()check, because of some behaviour in theabcmodule where it iterates through all of the subclasses ofcollections.abc.Mappingin order to determine whether an object is an instance ofcollections.abc.Mapping.The fix is to raise
TypeErrorbefore the class has actually been created, rather than during class initialisation.Why does this matter?
This might seem like an absurdly specific bug report, but it would be good to see it fixed. It was causing bizarre test failures in the
typing_extensionsbackport. The issue was that we did this in one test method:...And that final
Pclass wasn't being cleaned up by the garbage collector immediately after that test having been run. That then caused an unrelated test elsewhere to fail due to the fact thatisinstance([], collections.abc.Mapping)was evaluating toTrue, and it was very hard to figure out why.Linked PRs
isinstance([], collections.abc.Mapping)always evaluates toFalse#105281isinstance([], collections.abc.Mapping)always evaluates toFalse(GH-105281) #105318