About type-conversion special methods

Honestly, I don’t understand well this well. It’s quite subtle.

I’m more comfortable with examples (see below).

IMO __float__() should only return exact float instance, so the DeprecationWarning makes sense and we should now convert it to an error. Otherwise, the result is unclear and there are bad surprises, especially if the float subclass has a __float__ method. If __float__() returns a float subclass, __float__() is not called again which gives surprising results: see example 1 and example 2.

Is there any project in the wild which is impacted by the DeprecationWarning?

numpy is not affected by this issue:

$ python
>>> import numpy

>>> f64=numpy.float64(1.5)
>>> isinstance(f64, float)
True
>>> type(f64).__bases__
(<class 'numpy.floating'>, <class 'float'>)
>>> type(f64.__float__())
<class 'float'>

>>> f16=numpy.float16(1.5)
>>> isinstance(f16, float)
False
>>> type(f16.__float__())
<class 'float'>

Example 1:

class MyFloat(float):
    def __float__(self):
        return 42.

class NonFloat:
    def __init__(self):
        self.value = MyFloat(1.5)

    def __float__(self):
        return self.value

# (A) __float__() returns float
print(float(MyFloat(1.5)))
print()

# (B) __float__() returns MyFloat(float): emit DeprecationWarning
print(float(NonFloat()))

Output 1:

42.0

x.py:16: DeprecationWarning: NonFloat.__float__ returned non-float (type MyFloat).  The ability to return an instance of a strict subclass of float is deprecated, and may be removed in a future version of Python.
  print(float(NonFloat()))
1.5

It’s quite surprising that I get 42.0 in case (A) but 1.5 in case (B). It looks inconsistent to me.

Example 2:

class MyFloat(float):
    def __float__(self):
        return MyFloat(42)

# __float__() returns MyFloat(float): emit a DeprecationWarning
print(float(MyFloat(1.5)))

Output 2:

x.py:6: DeprecationWarning: MyFloat.__float__ returned non-float (type MyFloat).  The ability to return an instance of a strict subclass of float is deprecated, and may be removed in a future version of Python.
  print(float(MyFloat(1.5)))
42.0

It works as expected but it’s a little bit strange that the float() calls __float__() but then doesn’t call __float__() again on the float subclass. I’m not sure if I expect 1.5 or 42 result in this case :slight_smile: The deprecation warning sounds like “hey, something is wrong”: I agree :slight_smile:


If the float subclass has no __float__() method, obviously, things are simpler.

Example 3:

class MyFloat(float):
    pass

class NonFloat:
    def __init__(self):
        self.value = MyFloat(1.5)

    def __float__(self):
        return self.value

# (A) __float__() returns float
print(float(MyFloat(1.5)))
print()

# (B) __float__() returns MyFloat(float): emit DeprecationWarning
print(float(NonFloat()))

Output 3:

1.5

x.py:16: DeprecationWarning: NonFloat.__float__ returned non-float (type MyFloat).  The ability to return an instance of a strict subclass of float is deprecated, and may be removed in a future version of Python.
  print(float(NonFloat()))
1.5

@storchaka says that the DeprecationWarning doesn’t bring any value in this case. The conversion should be done silently.

Example 4:

class MyFloat(float):
    pass

# __float__() returns float
print(float(MyFloat(1.5)))

Output 4:

1.5
1 Like