Using The Python decimal Class for Accurate Calculations

Python decimal class

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:

0.8

But instead it prints:

0.7999999999999999

…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:

0.8

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:

new Decimal(value)

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:

0.1000000000000000055511151231257827021181583404541015625

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:

0.1

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)

For example:

Decimal((0, (2, 4, 1, 5), -3))

Is the same as:

returns Decimal('2.415')

And creates a decimal object with the value:

2.415

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:

Rounding Method
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:

28
ROUND_HALF_EVEN

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

Rounding

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:

1.2
1.1

…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:

4.5

Which is the correct answer, with the correct precision.

SHARE:
nv-author-image

Brad Morton

I'm Brad, and I'm nearing 20 years of experience with Linux. I've worked in just about every IT role there is before taking the leap into software development. Currently, I'm building desktop and web-based solutions with NodeJS and PHP hosted on Linux infrastructure. Visit my blog or find me on Twitter to see what I'm up to.

Leave a Reply

Your email address will not be published. Required fields are marked *