Documenting isinstance behavior with faked __class__

Background

The current documentation is fairly vague about how isinstance works:

Return True if the object argument is an instance of the classinfo argument, or of a (direct, indirect, or virtual) subclass thereof. If object is not an object of the given type, the function always returns False . If classinfo is a tuple of type objects (or recursively, other such tuples) or a Union Type of multiple types, return True if object is an instance of any of the types. If classinfo is not a type or tuple of types and such tuples, a TypeError exception is raised. TypeError may not be raised for an invalid type if an earlier check succeeds.

(Built-in Functions — Python 3.14.5 documentation)

The unittest.mock.Mock docs imply that overriding __class__ makes isinstance return True for the mocked type:

__class__

Normally the __class__ attribute of an object will return its type. For a mock object with a spec, __class__ returns the spec class instead. This allows mock objects to pass isinstance() tests for the object they are replacing / masquerading as:

(unittest.mock — mock object library — Python 3.14.5 documentation)

The comment in unittest.mock._is_instance_mock implies that faking __class__ overrides isinstance to ignore the true type of the object:

def _is_instance_mock(obj):
    # can't use isinstance on Mock objects because they override __class__
    # The base class for all mocks is NonCallableMock
    return issubclass(type(obj), NonCallableMock)

In practice, however, isinstance checks both type(obj) and obj.__class__.

object_isinstance implementation in CPython
static int
object_isinstance(PyObject *inst, PyObject *cls)
{
    PyObject *icls;
    int retval;
    if (PyType_Check(cls)) {
        retval = PyObject_TypeCheck(inst, (PyTypeObject *)cls);
        if (retval == 0) {
            retval = PyObject_GetOptionalAttr(inst, &_Py_ID(__class__), &icls);
            if (icls != NULL) {
                if (icls != (PyObject *)(Py_TYPE(inst)) && PyType_Check(icls)) {
                    retval = PyType_IsSubtype(
                        (PyTypeObject *)icls,
                        (PyTypeObject *)cls);
                }
                else {
                    retval = 0;
                }
                Py_DECREF(icls);
            }
        }
    }
    else {
        if (!check_class(cls,
            "isinstance() arg 2 must be a type, a tuple of types, or a union"))
            return -1;
        retval = PyObject_GetOptionalAttr(inst, &_Py_ID(__class__), &icls);
        if (icls != NULL) {
            retval = abstract_issubclass(icls, cls);
            Py_DECREF(icls);
        }
    }

    return retval;
}

(Objects/abstract.c:2602)

This has been the case since CPython 2.2, and PyPy matches the CPython behavior.

Proposal

I propose ammending the documentation for isinstance to more clearly reflect its actual behavior:

Return True if the object argument is an instance of the classinfo argument, or of a (direct, indirect, or virtual) subclass thereof. If the object argument has a __class__ attribute that differs from object’s actual type, additonally return True if __class__ is a (direct, indirect, or virtual) subclass of the classinfo argument. Otherwise, the function returns False. If classinfo

I would support clarifying the documentation.

But, your proposed wording doesn’t explain how __class__ works when a tuple or union is passed; it’s only mentioned in the section about individual classes.