Python 3 float precision and 8.5 - 8.4

Many programmers are surprised to learn that modern programming languages like Python still "calculate in wrong way":

8.5 - 8.4 = 0.099999999999999964

Actually the calculation itself is correct with correct value. The problem is representation and storage of 0.1 as a binary number. So the idea is: we can store 0.1 as 32 bits binary number with exact precision of 0.099999999999999964. This representation is fine enough for most cases but if you want to have a better representation and exact values then you can check following examples:

So about Python floating arithmetics we can conclude:

  • if you want better precision use Decimal or Numpy (slower than classic calculation - numpy is faster than decimal)
  • if you want to use classic way then you can format or round the final result

Pure float number in Python

This is the classic calculation of float numbers in Python. Which can lead to unexpected results like 0.09999999999999964. In order to overcome this problem you can round or use formatting of the final result. In case that you want a better precision then you can use libraries like numpy or Decimal(listed below):

print(8.5 - 8.4)

result

0.09999999999999964

Format float number in Python

Formating of float numbers allow precision which can solve the problem of the wrong calculation:

print('{}'.format(8.5 - 8.4))
print('{:.2f}'.format(0.1))
print('{}'.format(0.1))

result

0.09999999999999964
0.10
0.1  

Round float number in Python

Another way to workaorund the wrong value for 8.5 - 8.4 is by rounding. Rounding is useful and often used in financial systems. Where the floating point precision can be up to 7 or even 12th digit after the decimal point.

print(round(8.5 - 8.4, 2))
print(round(8.5 - 8.4, 20))

result

0.1
0.09999999999999964

Decimal and float number in Python

In Python Decimal will help you to get better precision working with floats at the price of slower performance:

from decimal import *
getcontext().prec = 28
print(Decimal('8.5') - Decimal('8.4'))

result

0.1

Performance profiling shows that Decimal is much slower in comparison to simple float subtraction:

  • 8.5 - 8.4 - 0.243 seconds
  • Decimal('8.5') - Decimal('8.4') - 6.058 seconds ( 8.741 seconds if you execute getcontext().prec = 28 in the for loop)
import cProfile
from decimal import *

def before():
    for i in range(1, 10000000):
        8.5 - 8.4


def after():
    getcontext().prec = 28
    for i in range(1, 10000000):
        Decimal('8.5') - Decimal('8.4')


cProfile.run('before()')
cProfile.run('after()')

This is the full output of profiling:

         4 function calls in 0.244 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.244    0.244 <string>:1(<module>)
        1    0.244    0.244    0.244    0.244 ProfilingSimple.py:4(before)
        1    0.000    0.000    0.244    0.244 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


         5 function calls in 6.058 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    6.058    6.058 <string>:1(<module>)
        1    6.058    6.058    6.058    6.058 ProfilingSimple.py:9(after)
        1    0.000    0.000    6.058    6.058 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method decimal.getcontext}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Numpy and float number in Python

Very popular library like numpy can help in this situation. You will need to install numpy to your python installation or environment (if you don't have it already) by pip:

python -m pip install --user numpy scipy matplotlib ipython jupyter pandas sympy nose

and use it like:

import numpy as np
print(np.float32(8.5 - 8.4))

result

0.1

From performance point of view you can see the using numpy can be much slower in comparison to classic float. For 10,000,000 times the times are:

  • 8.5 - 8.4 - 0.228 seconds
  • np.float32(8.5 - 8.4) - 3.524 seconds

So a better precision come at the price of performance:

import cProfile
import numpy as np

def before():
    for i in range(1, 10000000):
        8.5 - 8.4


def after():
    for i in range (1, 10000000):
        np.float32(8.5 - 8.4)

cProfile.run('before()')
cProfile.run('after()')
4 function calls in 0.228 seconds
1    0.228    0.228    0.228    0.228 ProfilingIsolation.py:7(before)

4 function calls in 3.524 seconds
1    3.524    3.524    3.524    3.524 ProfilingIsolation.py:12(after)

Related Article