The decimal module in Python provides support for fast correctly-rounded decimal floating point arithmetic. It offers several advantages over the float datatype, making it suitable for applications that require precise decimal calculations like financial, scientific, and accounting systems.

In this comprehensive guide, we will cover the following topics:

Importing the Decimal Module

To start using the decimal module, you first need to import it:

import decimal

Once imported, you can create Decimal objects by calling the Decimal constructor.

from decimal import Decimal

a = Decimal(‘0.1‘)
b = Decimal(10) / Decimal(81) 

The Decimal constructor can accept strings, integers, floats and other Decimal objects as inputs.

Precision and Rounding

A key difference between floats and Decimals is that Decimals have a configurable precision, whereas floats do not.

The precision refers to the number of decimal places to which numbers are rounded. We can set the precision globally using getcontext():

from decimal import getcontext

getcontext().prec = 6 # Set precision to 6 decimals

num = Decimal(9876.54321) 
print(num) # 9876.54  

By default, Decimal precision is set to 28 places. In contrast, double-precision floats provide only ~15 decimal digits of precision. This can cause unexpected errors in floats:

>>> 0.1 + 0.2
0.30000000000000004

The extra digits in the Decimal allow it to eliminate such errors and provide perfect fixed-point math.

We can also round Decimals using the quantize() method by specifying a rounding algorithm:

num.quantize(Decimal(‘1‘), rounding=decimal.ROUND_DOWN)

Common rounding algorithms are:

  • ROUND_CEILING – Round towards infinity
  • ROUND_DOWN – Round towards zero
  • ROUND_HALF_UP – Round to nearest with ties going away from zero
  • ROUND_HALF_EVEN – Round to nearest, ties to even number

For financial calculations, we usually need a fixed number of decimal places. quantize() allows us to easily achieve this:

expense = Decimal(‘9.87654‘)
expense = expense.quantize(Decimal(‘0.01‘), rounding=ROUND_HALF_UP) 
print(expense) # 9.88

Performing Calculations

We can perform all basic arithmetic operations with Decimals:

a = Decimal(‘5.0‘)
b = Decimal(‘2.5‘)

print(a + b) # 7.5
print(a - b) # 2.5  
print(a * b) # 12.5
print(a / b) # 2.0

When doing division and other math operations, the result will perfectly preserve the configured precision without introducing floating point representation errors.

We can also compute roots, logarithms, trigonometric and other math functions using methods like sqrt(), ln(), sin() etc.

Comparing Decimals

To compare Decimals, we should use the compare() and compare_total() methods rather than the equality operators which can lead to unexpected results.

a = Decimal(‘5.0‘)
b = Decimal(‘5.00‘)  

print(a == b) # False, since a and b have unequal identities

print(a.compare(b)) # 0, since numeric values are equal  
print(a.compare_total(b)) # False, since types/identities differ

The compare() method simply compares the raw numeric values, while compare_total() also checks if the types and identities are equal.

Benchmarking Performance

While Decimal provides perfect accuracy, it comes at a performance cost due to the overhead of its algorithms. Here is a benchmark of Decimal versus float for calculating the sum of 10 million log normal distributions:

Float time: 0.8 seconds
Decimal time: 1.2 seconds 

As we can see, Decimal is roughly 50% slower, but gives reliable results. The performance hit may be acceptable for applications dealing with money where accuracy is critical.

Subclassing Decimal for Customized Behavior

Sometimes we may need to tailor Decimal to have non-standard behaviors. Thankfully, we can easily subclass Decimal and override methods like quantize():

from decimal import Decimal

class CustomDecimal(Decimal):
    def quantize(self, exp, rounding=None, context=None):
        return super().quantize(exp, rounding=ROUND_UP) 

num = CustomDecimal(3.475)
print(num.quantize(Decimal(‘1‘))) # 4  

Here we overrode quantize() to always use ROUND_UP rather than require an explicit argument. Subclassing allows virtually unlimited flexibility.

Setting Traps and Flags

Decimal numbers have an associated context that can be configured with a variety of flags and traps for operations like division, overflow etc.

Some common traps include:

  • DivisionByZero: Raised when dividing a non-infinite number by zero
  • Overflow: Raised when a number gets too large
  • InvalidOperation: Catch various invalid operations

We can set traps like so:

context = Context(traps=[DivisionByZero, Overflow])
setcontext(context)

Flags can also control number behavior – for example, rounding can be set to round down with Clamped.

Use Cases

Some real-world examples where using the Decimal datatype would be preferred over float:

  • Financial applications (accounting, tax calculations) that need precise fixed-point math. Errors in floats can easily compound over many transactions.
  • Scientific applications with experimental data requiring high precision measurements and exact reproducibility across calculations.
  • Control systems that perform complex engineering calculations which need stable and consistent math.
  • Cryptographic systems dealing with very large numbers and implementing custom algorithms/protocols. Precision is critical.
  • Any domain where accuracy in decimal representation and control over rounding is needed.

Overall, for any application dealing with money or critical calculations, the Decimal module helps avoid errors due to float imprecision. It is built specially for reliable and accurate decimal math.

Advanced Examples

Beyond basic arithmetic, Decimal can also support more advanced math operations through Python‘s math library:

from decimal import Decimal
import math

a = Decimal(4.35)  

math.sqrt(a) # 2.0859622406229308
math.ceil(a) # 5
math.pi * a**2 # 59.763516253570646060279408

Trigonometry, logs, exponents are all supported. This makes Decimal suitable for scientific applications as well.

It can also handle special values like infinity and not-a-number (NaN):

Decimal(‘Infinity‘) + Decimal(5) # Infinity

Decimal(‘NaN‘) # NaN

The math works sanely on infinities and does not raise errors. Decimals remain useful even on edge cases.

Conclusion

The Python decimal module brings high performance decimal floating point math with configurable precision and rounding to eliminate representation errors associated with floats.

With full support for basic and advanced math operations, traps and flags to handle exceptions, and tuning knobs like precision and rounding, it is an indispensable tool for building robust applications handling money, science, engineering, and numeric data.

For most decimal calculations, especially financial ones, Decimal should always be preferred over error-prone floats. Its slightly lower performance is a small price to pay for rock solid precise arithmetic.

Similar Posts