Python’s `float`

type is a natural first step to represent monetary amounts in the code. Almost all platforms map Python floats to IEEE-754 “double precision”.

Doubles contain 53 bits of precision. When the machine is trying to represent the fractional part (mantissa) of a given number it finds a bit sequence `\(b_1, b_2 ... b_{53}\)`

so that a sum:

is close to the number as possible. So, values such as 0.1 cannot be exactly represented. You may find this famous example in official Python docs:

```
>>> .1 + .1 + .1 == .3
False
```

It happens because 0.1 is not exactly 1/10.

Let’s see how it can hinder money operations.

A web shop sells an item for 1.01. The web shop buys it form its supplier for 0.99 and the owner wants to calculate a revenue if quintillion items (one with 18 zeros) are sold:

```
format((1.01 - 0.99)*1e18, '.2f')
>>> '20000000000000016.00'
```

You expected a 2 with 16 zeros (2 cent revenue per item times 1e18), but there are an additional 16 dollars. It will take a lot of time to sell so many items, but the calculation is wrong anyway.

**More real life example**

You have a deposit of 93 cents in a bank with 2.25% interest rate. You came back in thousand years to withdraw it. Happened to this guy:

For that you need to calculate a future value:

```
deposit = 0.93
future_value = deposit * ((1 + (0.0225))**1000)
future_value
>>> 4283508449.7111807
```

Almost 4.3 billion. But is it accurate since we are using float here?

**Decimal**

It’s a part of a standard library and provides a representation of real numbers. The web shop revenue calculation is now correct:

```
from decimal import Decimal
sell_price = Decimal('1.01')
buy_price = Decimal('0.99')
items_sold = Decimal('1e18')
profit = (sell_price - buy_price) * items_sold
profit
>>> Decimal('2E+16')
```

And for future value:

```
deposit = Decimal('0.93')
decimal_future_value = deposit * (1 + (Decimal('0.0225')))**Decimal('1000')
decimal_future_value
>>> Decimal('4283508449.711328779924154334')
```

The difference with float in that case is quite small - 4th digit after decimal point. But it’s still present.

**Performance**

Float also appears more fast than Decimal:

```
In [2]: def float_order(item_price, item_count):
...: return sum([item_price for _ in range(item_count)])
...:
In [3]: def decimal_order(item_price, item_count):
...: decimal_item_price = Decimal(item_price)
...: return sum([decimal_item_price for _ in range(item_count)])
...:
In [4]: %timeit float_order(1.01, 10000)
297 µs ± 11.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [5]: from decimal import Decimal
In [6]: %timeit decimal_order('1.01', 10000)
783 µs ± 19.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

Have you ever experiences problems when working with monetary values represented by `float`

? Connect with me on LinkedIn

Support the author - Buy me a coffee!

Comments powered by Talkyard.