This guide will introduce you to the Python decimal library and class – Python tools which allow you to accurately work with decimal numbers without the rounding and accuracy issues which frequently arise when working with floating point numbers.
Computers Can’t Perform Accurate Arithmetic With Decimal Numbers
Decimal numbers are commonly stored as floating point number values. Computers are not particularly good at performing arithmetic with this type of numbers accurately.
We break down why this is in our Ultimate Beginners Guide to Floating Point Math/Arithmetic Precision Problems.
The Python Decimal Library
The Python decimal library ships with Python itself, and once imported, provides a convenient way to accurately represent and manipulate decimal numbers.
Demonstrating the Python Decimal Class
The decimal library contains the Decimal class – a variable class which can be used to represent a correctly rounded decimal number in Python.
Consider the following Python code which adds two decimal numbers:
print(0.1 + 0.7)
It should print the value:
But instead it prints:
…due to the rounding accuracy issues noted above.
By Using the Decimal class, this issue is avoided:
# Import the decimal library import decimal # Import the decimal class from decimal import Decimal # Create two decimal class variables for the numbers before adding them print(Decimal('0.1') + Decimal('0.7'))
This will return:
How to Use the Decimal Class
To perform calculations with Decimal objects, they need to first be created.
The syntax for creating a Python object using the decimal class is as follows:
Where value can be an integer or floating number, string, or tuple value.
Don’t Create Decimal Objects from Floating PointNumbers
Passing a floating point number to the decimal constructor completely invalidates the purpose of the decimal class, as the floating point number must first be converted to decimal, which brings up the binary accuracy issue outlined above:
num = Decimal(0.1) print(num)
The decimal object assigned to the variable num will have the inaccurate value:
Instead, Create Decimal Objects from Strings and Tuples
The decimal constructor can be used with strings containing numerical values. This is the easiest to read and use method of initializing decimal values without running into any precision issues:
num = Decimal('0.1') print(num)
The decimal value of the num variable will be:
Nice and accurate!
A tuple can also be used to create a decimal object. This is a bit harder to understand, but is useful in some data crunching scenarios where you want to be really specific about the of decimal value you are creating:
The tuple used in the decimal constructor should have 3 components – a sign (0 for positive or 1 for negative), a tuple of digits, and an integer exponent:
new Decimal(sign, (digit1, digit2, ...), exponent)
Decimal((0, (2, 4, 1, 5), -3))
Is the same as:
And creates a decimal object with the value:
Working with Decimal Contexts and Setting the Rounding Mechanism
Each Decimal class object is created within a context.
These contexts control the precision used when rounding numbers, and the method by which rounding occurs.
Setting the Context
Decimal contexts can be created and set using the Context constructor.
# Import the decimal library import decimal # Import the decimal class from decimal import Decimal, Context # Create a context which rounds to 20 decimal places, using the ROUND_HALF_UP method myContext = Context(prec=20, rounding=decimal.ROUND_HALF_UP) decimal.setcontext(myContext) print(Decimal(1) / Decimal(9))
Controlling the Precision
The precision should be an integer from 0 to 28 which specifies the number of digits after the decimal which number should use when rounded.
It is set using the context.prec value.
Controlling the Rounding Method
The following rounding methods are available:
|ROUND_UP||Round away from zero|
|ROUND_DOWN||Round towards zero|
|ROUND_CEILING||Round towards infinity|
|ROUND_FLOOR||Round towards negative infinity|
|ROUND_HALF_UP||Round to nearest with ties away from zero|
|ROUND_HALF_DOWN||Round to nearest with ties towards zero|
|ROUND_HALF_EVEN||round to nearest with ties to nearest even integer|
|ROUND_05UP||Round away from zero if last digit after rounding towards zero would have been 0 or 5 – otherwise round towards zero|
…and can be set using the context.rounding value.
Viewing the Current Context
You can see what the values are being currently used when working with the decimal class by viewing the context:
import decimal context = decimal.getcontext() print(context.prec) print(context.rounding)
Viewing the Default Context
The details for the default context used by the decimal class can be accessed in the same way:
import decimal context = decimal.DefaultContext print(context.prec) print(context.rounding)
Which will output:
Changing the Default Context
The default context for the current script can be altered, which will also apply to any further Python processes created:
import decimal decimal.DefaultContext.prec = 6 decimal. DefaultContext.rounding = decimal.ROUND_DOWN
The rounding method set in the decimal class context affects how numbers using that context are rounded.
import decimal from decimal import Decimal context = decimal.getcontext() num = Decimal('1.15') print(round(num, 1)) # Change the rounding method context.rounding = decimal.ROUND_HALF_DOWN print(round(num, 1))
This code will output:
…as you can see, the same number variable num is rounded, then the rounding method is changed in the context, then it is rounded again, producing different rounded values.
Performing Accurate Arithmetic with the Decimal Class
When you use Python’s built in math library, decimal objects will be converted to floating point numbers before any calculations are performed – resulting in a loss of precision – so watch out!
Otherwise, the standard arithmetic operators in Python can be used with decimal objects safely:
import decimal from decimal import Decimal print(Decimal('0.3') / Decimal('0.2') + Decimal('3'))
The above code will output:
Which is the correct answer, with the correct precision.