gh-153772: Make abc isinstance() tolerate instances without __class__ (#154149)
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).
diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py
index c870ae9..2bfb948 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 59a45a2..814d7ff 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 0000000..9393384
--- /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 5826efb..bca9066 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) {