Float vs Decimal in Python
Table of Contents
Introduction
Both the float and decimal types store numerical values in Python, and at the beginning, choosing when to use each can be confusing. Python’s decimal documentation is a good starting point to learn when to use decimals. Generally, decimals exist in Python to solve the precision issues of floats.
Floats
Use floats when convenience and speed matter. A float gives you an approximation of the number you declare. For example, if I print 0.1 with 18 decimals places, I don’t actually get 0.1 but instead an approximation.
>>> print(f"{0.1:.18f}")
0.100000000000000006
Similarly, when doing operations, such as addition with floats, you get an approximation, which can lead confusing code, such as the following.
>>> .1 + .1 + .1 == .3
False
>>> .1 + .1 + .1
0.30000000000000004
Intuitively, the addition makes sense, and at a glance, you expect the statement to be true. However, because of the float approximation it turns out to be false. This demonstrates one of the big issues with floats which is the lack of reliable equality testing. To fix this equality test without the use of decimals we need to use rounding.
>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True
>>> round(.1 + .1 + .1, 10)
0.3
In this case, we round the floats to prevent any precision issues. If you find yourself using floats and rounding frequently in your codebase, this indicates that it’s time to use decimals.
Decimals
Use decimals when precision matters, such as with financial calculations. Decimals can suffer from their own precision issues, but generally, decimals are more precise than floats. The performance difference between float and decimal, with Python 3, is not outlandish, and in my experience, the precision benefits of a decimal outweigh the performance benefits of a float.
Let’s look at the previous examples with decimals instead of floats.
>>> from decimal import Decimal
>>> print(f"{Decimal('0.1'):.18f}")
0.100000000000000000
>>> Decimal('.1') + Decimal('.1') + Decimal('.1') == Decimal('.3')
True
Using decimals in these examples prevents the subtle bugs introduced by floats. If you notice, the decimals use strings for initialization. Once again, using floats causes precision issues.
>>> from decimal import Decimal
>>> Decimal(0.01) == Decimal("0.01")
False
In this example, we expect these decimals to be equal, but, because of the precision issues with floats, this decimal equality test returns false. If we look at each of these decimals, we’ll see why.
>>> Decimal(0.01)
Decimal('0.01000000000000000020816681711721685132943093776702880859375')
>>> Decimal("0.01")
Decimal('0.01')
The decimal declared as a float is not technically 0.01, which results in an equality test of false. All decimals should be initialized using strings to prevent precision issues. If decimals aren’t initialized with strings, we lose some of the precision benefits of decimals and create subtle bugs.
Fractions
Edited 2021-01-15 credit Michael Amrhein
Michael points out that decimals suffer from their own precision issues with this example.
>>> from decimal import Decimal
>>> Decimal('1') / Decimal('3') * Decimal('3') == Decimal('1')
False
>>> Decimal('1') / Decimal('3') * Decimal('3')
Decimal('0.9999999999999999999999999999')
Michael also points out that the float equivalent does not run into precision issues.
>>> (1.0 / 3.0) * 3.0 == 1.0
True
Decimal have their own hidden rounding that cause precision issues, and to eliminate the hidden routing, you need to use Python’s fractions module.
>>> from fractions import Fraction
>>> Fraction('1') / Fraction('3') * Fraction('3') == Fraction('1')
True
The fractions module provides support for rational number arithmetic. Once again to avoid any precision issues, initialize the fraction with a string.
>>> Fraction(1.1) == Fraction("1.1")
False
Final Thoughts
For most use cases, I recommend using decimals. If you initialize them with strings, you prevent subtle bugs and get the increased precision benefits. If you need to eliminate all subtle rounding issues, use the fractions module. Even though floats perform better than decimals, I recommend avoiding floats.