Skip to content

Inconsistent behavior of object.__getattribute__ when applied to a type (builtin vs custom class) if attrs are not data descriptors #145907

@alphiy

Description

@alphiy

Bug report

Bug description:

When applying object.__getattribute__(cls, attr) to a type object, builtin class behave differently from custom class (or instances of class), i.e.

object.__getattribute__(builtin_class, attr)
behaves differently from
object.__getattribute__(custom_class, attr), object.__getattribute__(instance_of_class, attr)

It seems like that object.__getattribute__(builtin_class, attr) doesn't lookup its own dict of builtin class before checking up dict of its metaclass' mro.

# builtin class case
assert '__mul__' in vars(int)
object.__getattribute__(int, "__mul__")
Out: AttributeError: 'type' object has no attribute '__mul__'

# custom class case
class A(int):
    def __mul__(self, *args, **kwargs):
        return super().__mul__(*args, **kwargs)
assert '__mul__' in vars(A)
object.__getattribute__(A, "__mul__") 
Out: <function __main__.A.__mul__(self, *args, **kwargs)> 

After diving into dict checking part of _PyObject_GenericGetAttrWithDict() (v3.14), since metaclass of builtin class is type, which hasn't turn on Py_TPFLAGS_INLINE_VALUES and Py_TPFLAGS_MANAGED_DICT, so the dicts of builtin class or custom class are all treated as computed_dict by dictptr = _PyObject_ComputedDictPointer(obj);. The problem is that the dict of builtin class turns out to be the managed_dict, their tp_dict slot always point to NULL, so it seems like skipping check of their own tp_dict, and finally lead to this kind of inconsistent behavior.

The primitive fix is below, and it works on v3.14

--- a/Objects/object.c
+++ b/Objects/object.c
@@ -1733,6 +1733,21 @@ _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
         else if ((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT)) {
             dict = (PyObject *)_PyObject_GetManagedDict(obj);
         }
+        else if (tp->tp_flags & Py_TPFLAGS_TYPE_SUBCLASS) {
+            PyTypeObject * obj_as_type = _Py_CAST(PyTypeObject*, obj);
+
+            /* exactly the same as
+             * dict = lookup_tp_dict(obj_as_type) */
+            if (obj_as_type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
+                PyInterpreterState *interp = _PyInterpreterState_GET();
+                managed_static_type_state *state = _PyStaticType_GetState(interp, obj_as_type);
+                assert(state != NULL);
+                dict = state->tp_dict;
+            }
+            else {
+                dict = obj_as_type->tp_dict;
+            }
+        }
         else {
             PyObject **dictptr = _PyObject_ComputedDictPointer(obj);
             if (dictptr) {
object.__getattribute__(int, "__mul__")
Out: <slot wrapper '__mul__' of 'int' objects>

Since type objects are only the legitimate self argument of type.__getattribute__, it will raise an error when applied to instances of class. However since everything in Python is object, so everything is legitimate self argument of object.__getattribute__, although it is rare case when applied to type objects in routine usage. And in routine practice, dot operator and getattr() always guarantee instances use __getattribute__ of class and type objects use __getattribute___ of metaclass. So I don't know whether it worth fixing, I just point it out here.

CPython versions tested on:

3.14, 3.12

Operating systems tested on:

Linux, Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions