Why does the floating-point value of 4*0.1 look nice in Python 3 but 3*0.1 doesn't?
I know that most decimals don't have an exact floating point representation (Is floating point math broken?).
But I don't see why 4*0.1
is printed nicely as 0.4
, but 3*0.1
isn't, when
both values actually have ugly decimal representations:
>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Solution 1:
The simple answer is because 3*0.1 != 0.3
due to quantization (roundoff) error (whereas 4*0.1 == 0.4
because multiplying by a power of two is usually an "exact" operation). Python tries to find the shortest string that would round to the desired value, so it can display 4*0.1
as 0.4
as these are equal, but it cannot display 3*0.1
as 0.3
because these are not equal.
You can use the .hex
method in Python to view the internal representation of a number (basically, the exact binary floating point value, rather than the base-10 approximation). This can help to explain what's going on under the hood.
>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'
0.1 is 0x1.999999999999a times 2^-4. The "a" at the end means the digit 10 - in other words, 0.1 in binary floating point is very slightly larger than the "exact" value of 0.1 (because the final 0x0.99 is rounded up to 0x0.a). When you multiply this by 4, a power of two, the exponent shifts up (from 2^-4 to 2^-2) but the number is otherwise unchanged, so 4*0.1 == 0.4
.
However, when you multiply by 3, the tiny little difference between 0x0.99 and 0x0.a0 (0x0.07) magnifies into a 0x0.15 error, which shows up as a one-digit error in the last position. This causes 0.1*3 to be very slightly larger than the rounded value of 0.3.
Python 3's float repr
is designed to be round-trippable, that is, the value shown should be exactly convertible into the original value (float(repr(f)) == f
for all floats f
). Therefore, it cannot display 0.3
and 0.1*3
exactly the same way, or the two different numbers would end up the same after round-tripping. Consequently, Python 3's repr
engine chooses to display one with a slight apparent error.
Solution 2:
repr
(and str
in Python 3) will put out as many digits as required to make the value unambiguous. In this case the result of the multiplication 3*0.1
isn't the closest value to 0.3 (0x1.3333333333333p-2 in hex), it's actually one LSB higher (0x1.3333333333334p-2) so it needs more digits to distinguish it from 0.3.
On the other hand, the multiplication 4*0.1
does get the closest value to 0.4 (0x1.999999999999ap-2 in hex), so it doesn't need any additional digits.
You can verify this quite easily:
>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True
I used hex notation above because it's nice and compact and shows the bit difference between the two values. You can do this yourself using e.g. (3*0.1).hex()
. If you'd rather see them in all their decimal glory, here you go:
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Solution 3:
Here's a simplified conclusion from other answers.
If you check a float on Python's command line or print it, it goes through function
repr
which creates its string representation.Starting with version 3.2, Python's
str
andrepr
use a complex rounding scheme, which prefers nice-looking decimals if possible, but uses more digits where necessary to guarantee bijective (one-to-one) mapping between floats and their string representations.This scheme guarantees that value of
repr(float(s))
looks nice for simple decimals, even if they can't be represented precisely as floats (eg. whens = "0.1")
.At the same time it guarantees that
float(repr(x)) == x
holds for every floatx