If you need precision in your project you should use decimal —decimal 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

Decimal has better roundingmethods. See below;

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(result.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)
# Output: 5.86

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
from typing import Optional


class Acccounting:
    """General class for money operations"""

    @classmethod
    def round(self, amount: str, ndigits: Optional[int] = 0) -> 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.money 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!


Changelog

  • 2021-12-02 :
    • Make round classmethod
    • Fix missing var result
    • Update import path for helper module