There are special methods used in implicit and explicit type conversions. __str__, __bytes__, __int__, __index__, __float__, __complex__.
__index__,__float__and__complex__– for implicit convertion toint,floatandcomplex. Most of the C API which convert these Python types to corresponding C types accept also objects that implement these special methods and use them if necessary.__str__,__bytes__and__int__– only for explicit conversion bystr(),bytes()andint().
Now, there are some questions: when to the special method and how to handle its result.
Look, for example, at PyFloat_AsDouble(). If the argument is a Python float, it gets the C double value directly from the ob_val field of the PyFloatObject structure. This works also for float subclasses, becaus ethey have the same structure. If the argument if not a Python float, then the __float__ method is used, and the C double value is obtained from its result. It can also fall back to using __index__, but this is not relevant here.
If the result of __float__ is not a Python float, then this is error. __float__ is not called recursively, this could be unsafe.
Note two things:
__float__is not called forfloatsubclasses. It is not necessary, we already havePyFloatObject. Calling__float__would be a waste of time. It is even more true for long integers, strings, etc.- Returning a
floatsubclass from__float__is not an error, fro the same reason. We only need to readPyFloatObject.ob_val, it is the same for subclasses as for exactfloat.
However, there was one problem. If str() just returns the result of __str__, it may return a subclass of str, and this is not whet we expect. On Python level we expect that str() always returns an exact str, this is the way to convert the object to exact str. There was a simiular issue with operator.index() and __index__.
The decision taken to solve this problem was two-step. If the result of the special methos is not an exact base type, but its subtype:
- Convert it to the base type ignoring its type and only using its content.
- Emit a deprecation warning.
BTW, __str__ and __bytes__ were overlooked, so we still have a problem of returning an str subclass from str(), bytes() and repr().
I think the deprecation was wrong. Silent conversion in step 1 was enough (it can be omitted if we need not a Python object, but just its content, as in PyFloat_AsDouble()). Deprecation just adds an inconvenience for users. For example, let we have an int-like object which implents __index__ that returns a calculated value. If the value happens to be not exact int, but int subclass (for example bool or IntEnum), the user is forced to convert it to int before returning from __index__. In worst case they will use int() for conversion, which can silently truncate the non-integer result, hiding the real bug. In any case, explicit conversion to int takes time and creates unnecesary object. Even if the exact int is needed (for example if we use operator.index()), it is faster to create in C than call operator.index() or int() in Python. And in most cases it should only be converted to C integer, so no need to create an intermediate Python object.
So my suggestion – just remove the deprecation. The code that already worked will continue to work. No new errors will occur. Most users will not notice anything. But the future user code could be cleaner and faster.