Decimal Precision and Rounding in Python

If you need precision in your project you should use decimaldecimal module, since float data type has some issues and limitations as you can see here: floating issues.

To use decimal as described here decimal tutorial:

from decimal import Decimal


number = Decimal("0.976815352")

OK, if I want some specific precision what should I do? With float it's something like:

number = 0.976815352
rounded_number = (number, '.2f')
# or
print(f"rounded_number = {x:.2f}")

With decimal:

from decimal import getcontext, Decimal


getcontext().prec = 3
result = Decimal("0.976815352") / Decimal("2.42532")

print(result)
# 0.403

will give 0.403. It seems OK. However if you try

result = Decimal('3.1415926535') + Decimal('2.7182818285')

print(result)
# 5.86

will give 5.86 and you'll probably say wth. It's because in decimal, precision means precision including the digits before decimal/dot.

So how is it possible to know the digits of a decimal among thousands, e.g. price between ∞ and 0.00000001?

After a lot of researching, a.k.a. stackoverflowing, I came across to this: gist.

Precision & Rounding

Option 1 - Round Decimal Using round()

from decimal import Decimal


result = Decimal('3.1415926535') + Decimal('2.7182818285')
result = round(x, 2)

print(result)
# Output: 5.86

Option 2 - Use Decimal Module's Roundings

from decimal import Decimal, ROUND_HALF_UP
# All options for rounding:
# ROUND_05UP       ROUND_DOWN       ROUND_HALF_DOWN  ROUND_HALF_UP
# ROUND_CEILING    ROUND_FLOOR      ROUND_HALF_EVEN  ROUND_UP


result = Decimal('3.1415926535') + Decimal('2.7182818285')
result = Decimal(our_value.quantize(Decimal('.01'), rounding=ROUND_HALF_UP))

print(result)
# Output: 5.86

We use "0.01" as round to 2 digits decimal.

Option 3 - Setting Getcontext Precision

We saw this one in the beginning and it gave us wth:

from decimal import getcontext, Decimal


getcontext().prec = 2
result = Decimal('3.1415926535') + Decimal('2.7182818285')

print(result)

So better not to chose because of confusion.

Option 4 (Preferred Option)

After reading some comments on the gist I found my ultimate solution. It
comes from py-moneyd library:

def round(self: M, ndigits: Optional[int] = 0) -> M:
    """
    Rounds the amount using the current ``Decimal`` rounding algorithm.
    """
    if ndigits is None:
        ndigits = 0
    return self.__class__(
        amount=self.amount.quantize(Decimal("1e" + str(-ndigits))),
        currency=self.currency,
    )

I did some tweaks for generalization and I wrote this helper method:

from decimal import Decimal


class Acccounting:
    def round(self, amount: str, ndigits: int) -> Decimal:
        """ Rounds the amount using the current `Decimal` rounding algorithm. """
        if ndigits is None:
            ndigits = 0
        return Decimal(
            Decimal(amount).quantize(Decimal("1e" + str(-ndigits))),
        )

In order to round 2 with this helper:

from decimal import Decimal
from .helpers import Acccounting


result = Decimal('3.1415926535') + Decimal('2.7182818285')
result = Acccounting().round(amount=result, ndigits=2)

print(result)
# Output: 5.86

Thanks a lot to @jackiekazil !

All done!

14