Back to articles list Articles
6 minutes read

How to Count Money Exactly in Python

Using floats to do exact calculations in Python can be dangerous. Here, we explain why and show you an alternative solution.

When you try to find out how much 2.32 x 3 is, Python tells you it's 6.959999999999999. For some calculations, that’s fine. But if you are calculating a transaction involving money, that’s not what you want to see. Sure, you could round it off, but that's a little hacky.

In this article, we show you how to get exact calculations by importing the decimal module. To see why this is helpful, we first need to cover some background on how numbers are represented in Python.

Decimal and Binary Numbers

A floating-point number is simply a number with a decimal point in it. In Python, you can check if a number is a float by using the built-in function type():

>>> type(1)
<class 'int'>
>>> type(1.0)
<class 'float'>

The built-in functions float() and int() can convert other data types, including strings, into floating point numbers and integers respectively:

	print(float(1))
	print(float('1.10'))
	print(int(1.1))

When you enter a floating-point number into Python, it is expressed in the base 10 (decimal) number system. Take the decimal fraction 0.1234, which can be represented as 1/101 + 2/102 + 3/103 + 4/104. However, computer hardware stores numbers in base 2 (binary). The binary fraction 0.1011 (with a decimal equivalent of 0.6875), for example, can be expressed as 1/21 + 0/22 + 1/23 + 1/24.

The problem is most decimal fractions cannot be represented exactly as binary fractions. This means most of the floating-point numbers you enter into Python can only be approximated by a binary floating-point number stored on the machine.

Consider the decimal float 0.1. As a decimal fraction, this can be perfectly represented by 1/101, but the closest binary fractional representation is 3602879701896397/255. When you enter this into the Python terminal, the value is rounded to 0.1 to display it, which can be misleading, because the value stored on the machine is, in fact: 0.1000000000000000055511151231257827021181583404541015625

Counting Money in Python

You can probably start to see how this could become problematic. Consider the following scenario regarding counting money in Python:

unit_price = 2.32
number_sold = 3
money_received = 6.96
if unit_price * number_sold == money_received:
    print('Accounts balanced')
else:
    raise ValueError('Accounts not balanced')

Here, unit_price and money_received are floats, and number_sold is an integer. The books are indeed balanced in this example. However, this produces an error due to how floats are represented and stored on your machine, which indicates there is a discrepancy between the money received and the amount of the sale.

This is where the decimal module comes in handy. It comes with the default Python installation and has several classes. The relevant one for our problem is the Decimal() class. The advantage of using this module is that decimal numbers can be represented exactly in Python. This exactness also applies to the results of arithmetic.

So, to solve the above problem, this class can be imported and used as follows:

from decimal import Decimal
unit_price = Decimal('2.32')
number_sold = 3
money_received = Decimal('6.96')
if unit_price * number_sold == money_received:
    print('Accounts balanced')
else:
    raise ValueError('Accounts not balanced')

Notice here the input value to Decimal() is a string, but other data types can also be used as an input. In the example above, if you define the variable as unit_price = Decimal(2.32), you end up with an inexact representation of 2.32, since the floating-point number is converted to its binary decimal equivalent.

Take the float 0.1 as an input. Entering Decimal(0.1) into the Python terminal returns Decimal('0.1000000000000000055511151231257827021181583404541015625'). We came across this representation earlier. So, to count money exactly in Python, it’s better and more intuitive to use strings as the input.

By the way, if you want to learn how to work with strings in Python, check out this course.

The variables unit_price and money_received are now Decimal objects, with a new data type decimal.Decimal. Check for yourself using the built-in function type() as we did above.

These Decimal objects have many of the same properties as other data types like floats and integers. The normal mathematical operations apply, and the results of these operations have an exact representation, which is what fixed the problem of balancing our books. And like other data types, Decimal objects can be used in a list or dictionary, and they can be copied, printed, sorted, or converted into other data types.

Precision

For many applications involving counting money exactly in Python, it’s nice to be able to set the precision of the numbers. The decimal module provides the user with the ability to do just this, with a default value of 28 decimal digits. Floating-point numbers, on the other hand, only have a precision of typically less than about 15 digits. Setting the precision is straightforward:

from decimal import *
getcontext().prec = 10

The module includes the concept of significant places, so the result of Decimal('1.10') + Decimal('20.20') is Decimal('21.30'), with the trailing zero kept for consistency. Additionally, the module includes the ability to define how numbers are rounded. This is also handled with getcontext(). You can read more in the official documentation.

Decimal Objects and Methods

Decimal objects have several methods which help with further calculations, conversions, and checks. For example, there are methods to calculate the square root, the exponential function, and the logarithm. You can also find the maximum and minimum values, use logical operations, and do checks for finite and NaN values, among others.

A lot of these will be familiar to users of other Python packages such as numpy (great for working with arrays), math (great for working with scalars), or pandas (great for data cleaning and analysis). But using the decimal module provides all the advantages discussed above.

Two methods that help handle formatting are normalize() and quantize(). The former strips the rightmost trailing zeros and standardizes the format of the object. For example, Decimal('1.10').normalize() and Decimal('0.11e1').normalize() both return Decimal('1.1').

On the other hand, the latter function, quantize(), is used to round off values to match the exponent of the argument, so that the two values have the same number of decimal places, without you having to know how many in advance.

	>>> value1 = Decimal('1.01')
	>>> value2 = Decimal('10.001')
	>>> value1.quantize(value2)
Decimal('1.010')
	>>> value2.quantize(value1)
Decimal('10.00')

But a word of caution: only quantize the final result of a calculation to prevent small rounding errors from accumulating.

Counting Money in Python: Where to Now?

We have reviewed the use of the decimal module to count money in Python, but there are also other Python modules worth mentioning. For example, fractions supports exact calculations by implementing rational-number-based arithmetic.

The Python modules money, prices, and py-moneyed provide extra functionality for money management based on the decimal module. They all support handling different currencies, so you can avoid accidentally adding euros to dollars without accounting for the exchange rate. Check them out to see which one best fits your needs. Given how automated finance has become, having the decimal module, with the decimal.Decimal data type, and additional functionality built-in to Python, is a great advantage for many projects.

You can also build on the decimal functionality and create your own project to handle your finances. A good starting point for an aspiring data scientist is this detailed course, which touches on many topics we have discussed here.

Working with numbers, and doing exact calculations in Python is great, but most results need to be visualized to understand them properly. We have some great introductory material in data visualization worth looking into. Check out part 1 and part 2 here.