From eb940c016321c36f448667c12e3cc14c1a55d6b9 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sun, 19 Jul 2026 17:41:01 +0300 Subject: [PATCH] gh-153772: Make abc isinstance() tolerate instances without __class__ The built-in isinstance() reads an instance's __class__ with a lookup that suppresses AttributeError and falls back to the object's type, so isinstance(obj, int) returns False for an object whose __class__ access raises. ABCMeta.__instancecheck__ read __class__ directly instead, so isinstance(obj, Mapping) leaked that AttributeError. Fall back to type(instance) when __class__ is unavailable, in both the C and the pure-Python implementations, so the abstract base classes behave like the built-in isinstance(). Such objects are unusual, but they do turn up in the wild (for example some Qt widgets). --- Lib/_py_abc.py | 7 ++++++- Lib/test/test_abc.py | 19 +++++++++++++++++++ ...-07-19-17-05-00.gh-issue-153772.cobjTK.rst | 5 +++++ Modules/_abc.c | 8 ++++++-- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b4f13..2bfb9489a7acdcb 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -92,7 +92,12 @@ def _abc_caches_clear(cls): def __instancecheck__(cls, instance): """Override for isinstance(instance, cls).""" # Inline the cache checking - subclass = instance.__class__ + try: + subclass = instance.__class__ + except AttributeError: + # Fall back to the type when the instance has no __class__, + # matching the behaviour of the built-in isinstance() (gh-153772). + subclass = type(instance) if subclass in cls._abc_cache: return True subtype = type(instance) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 59a45a2eda07b00..814d7fff2f41351 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -380,6 +380,25 @@ class C(str): pass self.assertIsSubclass(C, A) self.assertIsSubclass(C, (A,)) + def test_instancecheck_no_class(self): + # gh-153772: __instancecheck__ must fall back to type(instance) + # when the instance has no __class__, matching isinstance(). + class NoClass: + def __getattribute__(self, name): + if name == "__class__": + raise AttributeError(name) + return super().__getattribute__(name) + + class A(metaclass=abc_ABCMeta): + pass + + obj = NoClass() + # Must return False rather than propagating the AttributeError. + self.assertNotIsInstance(obj, A) + # Registering the actual type makes the fallback report a match. + A.register(NoClass) + self.assertIsInstance(obj, A) + def test_registration_edge_cases(self): class A(metaclass=abc_ABCMeta): pass diff --git a/Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst b/Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst new file mode 100644 index 000000000000000..9393384151450d8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst @@ -0,0 +1,5 @@ +:func:`isinstance` checks against :mod:`collections.abc` classes such as +:class:`~collections.abc.Mapping` no longer raise :exc:`AttributeError` +when the instance has no ``__class__``. The abstract base class machinery +now falls back to the object's type in that case, matching the behaviour of +the built-in :func:`isinstance`. diff --git a/Modules/_abc.c b/Modules/_abc.c index 5826efbfecb6901..bca9066d340ee43 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -629,11 +629,15 @@ _abc__abc_instancecheck_impl(PyObject *module, PyObject *self, return NULL; } - subclass = PyObject_GetAttr(instance, &_Py_ID(__class__)); - if (subclass == NULL) { + if (PyObject_GetOptionalAttr(instance, &_Py_ID(__class__), &subclass) < 0) { Py_DECREF(impl); return NULL; } + if (subclass == NULL) { + /* Fall back to the type when the instance has no __class__, matching + the behaviour of the built-in isinstance() (gh-153772). */ + subclass = Py_NewRef((PyObject *)Py_TYPE(instance)); + } /* Inline the cache checking. */ int incache = _in_weak_set(impl, &impl->_abc_cache, subclass); if (incache < 0) {