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
,float
andcomplex
. 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 forfloat
subclasses. 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
float
subclass 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.