Decimal Precision and Rounding in Python
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 rounding
methods. 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
- Make