3 Answers
3

I suspect it has to do with a lookup optimization. From the source code:

/* speed hack: we could use lookup_maybe, but that would resolve the
method fully for each attribute lookup for classes with
__getattr__, even when the attribute is present. So we use
_PyType_Lookup and create the method only when needed, with
call_attribute. */
getattr = _PyType_Lookup(tp, getattr_str);
if (getattr == NULL) {
/* No __getattr__ hook: use a simpler dispatcher */
tp->tp_getattro = slot_tp_getattro;
return slot_tp_getattro(self, name);
}

The fast path does does not look it up on the class dictionary.

Therefore, the best way to get the desired functionality is to place an override method in the class.

It's a known (and maybe not so well) documented difference. PyPy does not differentiate between functions and builtin functions. In CPython functions get binded as unbound methods when stored on the class (the have __get__), while builtin functions don't (they're different).

Under PyPy however, builtin functions are exactly the same as python functions, so the interpreter can't tell them apart and treats them as python-level functions. I think this was defined as implementation details, although there was some discussion on python-dev about removing this particular difference.

The mechanism behind the behavior in question is (probably, I'm no expert) outside the 'clean' subset of Python (as documented in thorough books like 'Learning Python' or 'Python in a nutshell' and somewhat loosely specified at python.org) and pertains to the part of the language that is documented 'as-is' by the implementation (and is subject to (rather) frequent changes).