Decimal Fixed Precision

- 1 answer

I would like to use decimal in currency calculations, so I would like to work on exactly two numbers after comma. Initially I thought that prec of decimal's context refers to that property, but after a few experiments I feel a bit confused.

Experiment #1:

In [1]: import decimal                                                                                                                      

In [2]: decimal.getcontext().prec = 2                                                                                                       

In [3]: a = decimal.Decimal('159.9')                                                                                                        

In [4]: a                                                                                                                                   
Out[4]: Decimal('159.9')

In [5]: b = decimal.Decimal('200')                                                                                                          

In [6]: b                                                                                                                                   
Out[6]: Decimal('200')

In [7]: b - a                                                                                                                               
Out[7]: Decimal('40')

Experiment #2:

In [8]: decimal.getcontext().prec = 4                                                                                                       

In [9]: a = decimal.Decimal('159.9')                                                                                                        

In [10]: a                                                                                                                                  
Out[10]: Decimal('159.9')

In [11]: b = decimal.Decimal('200')                                                                                                         

In [12]: b                                                                                                                                  
Out[12]: Decimal('200')

In [13]: b - a                                                                                                                              
Out[13]: Decimal('40.1')

Experiment #3: (prec is still set to 4)

In [14]: a = decimal.Decimal('159999.9')                                                                                                    

In [15]: a                                                                                                                                  
Out[15]: Decimal('159999.9')

In [16]: b = decimal.Decimal('200000')                                                                                                      

In [17]: b                                                                                                                                  
Out[17]: Decimal('200000')

In [18]: b - a                                                                                                                              
Out[18]: Decimal('4.000E+4')

Why does it work like in my examples? How should I work with decimals in my (currency calculations) case?



The precision sets the number of significant digits, which is not equivalent to the number of digits after the decimal point.

So if you have a precision of 2, you'll have two significant digits, so a number with 3 significant digits like 40.1 will be reduced to the upper two significant digits giving 40.

There's no easy way to set the number of digits after the decimal point with Decimal. However you could use a high precision and always round your results to two decimals:

>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 60  # use a higher/lower one if needed
>>> Decimal('200') - Decimal('159.9')
>>> r = Decimal('200') - Decimal('159.9')
>>> round(r, 2)

The decimal FAQ also includes a similar question and answer (using quantize):

Q. In a fixed-point application with two decimal places, some inputs have many places and need to be rounded. Others are not supposed to have excess digits and need to be validated. What methods should be used?

A. The quantize() method rounds to a fixed number of decimal places. If the Inexact trap is set, it is also useful for validation:

>>> TWOPLACES = Decimal(10) ** -2       # same as Decimal('0.01')
>>> # Round to two places
>>> Decimal('3.214').quantize(TWOPLACES)
>>> # Validate that a number does not exceed two places
>>> Decimal('3.21').quantize(TWOPLACES, context=Context(traps=[Inexact]))
>>> Decimal('3.214').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Traceback (most recent call last):
Inexact: None

Q. Once I have valid two place inputs, how do I maintain that invariant throughout an application?

A. Some operations like addition, subtraction, and multiplication by an integer will automatically preserve fixed point. Others operations, like division and non-integer multiplication, will change the number of decimal places and need to be followed-up with a quantize() step:

>>> a = Decimal('102.72')           # Initial fixed-point values
>>> b = Decimal('3.17')
>>> a + b                           # Addition preserves fixed-point
>>> a - b
>>> a * 42                          # So does integer multiplication
>>> (a * b).quantize(TWOPLACES)     # Must quantize non-integer multiplication
>>> (b / a).quantize(TWOPLACES)     # And quantize division

In developing fixed-point applications, it is convenient to define functions to handle the quantize() step:

>>> def mul(x, y, fp=TWOPLACES):
...     return (x * y).quantize(fp)
>>> def div(x, y, fp=TWOPLACES):
...     return (x / y).quantize(fp)

>>> mul(a, b)                       # Automatically preserve fixed-point
>>> div(b, a)