Skip to content

Commit 3e245fa

Browse files
authored
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).
1 parent de2ea9a commit 3e245fa

4 files changed

Lines changed: 36 additions & 3 deletions

File tree

Lib/_py_abc.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,12 @@ def _abc_caches_clear(cls):
9292
def __instancecheck__(cls, instance):
9393
"""Override for isinstance(instance, cls)."""
9494
# Inline the cache checking
95-
subclass = instance.__class__
95+
try:
96+
subclass = instance.__class__
97+
except AttributeError:
98+
# Fall back to the type when the instance has no __class__,
99+
# matching the behaviour of the built-in isinstance() (gh-153772).
100+
subclass = type(instance)
96101
if subclass in cls._abc_cache:
97102
return True
98103
subtype = type(instance)

Lib/test/test_abc.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,25 @@ class C(str): pass
380380
self.assertIsSubclass(C, A)
381381
self.assertIsSubclass(C, (A,))
382382

383+
def test_instancecheck_no_class(self):
384+
# gh-153772: __instancecheck__ must fall back to type(instance)
385+
# when the instance has no __class__, matching isinstance().
386+
class NoClass:
387+
def __getattribute__(self, name):
388+
if name == "__class__":
389+
raise AttributeError(name)
390+
return super().__getattribute__(name)
391+
392+
class A(metaclass=abc_ABCMeta):
393+
pass
394+
395+
obj = NoClass()
396+
# Must return False rather than propagating the AttributeError.
397+
self.assertNotIsInstance(obj, A)
398+
# Registering the actual type makes the fallback report a match.
399+
A.register(NoClass)
400+
self.assertIsInstance(obj, A)
401+
383402
def test_registration_edge_cases(self):
384403
class A(metaclass=abc_ABCMeta):
385404
pass
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:func:`isinstance` checks against :mod:`collections.abc` classes such as
2+
:class:`~collections.abc.Mapping` no longer raise :exc:`AttributeError`
3+
when the instance has no ``__class__``. The abstract base class machinery
4+
now falls back to the object's type in that case, matching the behaviour of
5+
the built-in :func:`isinstance`.

Modules/_abc.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,11 +629,15 @@ _abc__abc_instancecheck_impl(PyObject *module, PyObject *self,
629629
return NULL;
630630
}
631631

632-
subclass = PyObject_GetAttr(instance, &_Py_ID(__class__));
633-
if (subclass == NULL) {
632+
if (PyObject_GetOptionalAttr(instance, &_Py_ID(__class__), &subclass) < 0) {
634633
Py_DECREF(impl);
635634
return NULL;
636635
}
636+
if (subclass == NULL) {
637+
/* Fall back to the type when the instance has no __class__, matching
638+
the behaviour of the built-in isinstance() (gh-153772). */
639+
subclass = Py_NewRef((PyObject *)Py_TYPE(instance));
640+
}
637641
/* Inline the cache checking. */
638642
int incache = _in_weak_set(impl, &impl->_abc_cache, subclass);
639643
if (incache < 0) {

0 commit comments

Comments
 (0)