TL; DR: Attribute searches begin with a call to __getattribute__ , and it does all the work mentioned in the links. Based on the type of the object, either object.__getattribute__ or type.__getattribute__ .
Any attribute search result results in LOAD_ATTR bytecode inside.
>>> import dis >>> dis.dis(lambda: foo.x) 1 0 LOAD_GLOBAL 0 (foo) 2 LOAD_ATTR 1 (x) 4 RETURN_VALUE
LOAD_ATTR , then calls PyObject_GetAttr .
PyObject_GetAttr now searches for either tp_getattro or tp_getattr (deprecated) object type slot.
PyObject * PyObject_GetAttr(PyObject *v, PyObject *name) { PyTypeObject *tp = Py_TYPE(v); ... if (tp->tp_getattro != NULL) return (*tp->tp_getattro)(v, name); if (tp->tp_getattr != NULL) { const char *name_str = PyUnicode_AsUTF8(name); if (name_str == NULL) return NULL; return (*tp->tp_getattr)(v, (char *)name_str); } ... }
Now, if the object has its own implementation of __getattribute__ , then it is used differently, it returns to object.__getattribute__ or type.__getattribute__ based on the type. tp_getattro slot in the case of object points to PyObject_GenericGetAttr and for type it points to type_getattro .
PyObject_GenericGetAttr and type_getattro basically do all the work of checking descriptors, dictionary, slots, etc. (based on type) and try to return the value. If they cannot find it even after all, then an AttributeError will be raised, and if the type of the object is defined by __getattr__ , then it will be called.
source share