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) {