29

How do I get my decimals to stay at 2 places for representing money using the decimal module?

I've setting the precision, and damn near everything else, and met with failure.

Musaab
  • 1,574
  • 3
  • 18
  • 37

5 Answers5

53

When working with money you usually want to limit precision as late as possible so things like multiplication don't aggregate rounding errors. In python 2 and 3 you can .quantize() a Decimal to any precision you want:

unit_price = decimal.Decimal('8.0107')
quantity = decimal.Decimal('0.056')
price = unit_price * quantity
cents = decimal.Decimal('.01')
money = price.quantize(cents, decimal.ROUND_HALF_UP)
Ryan Allen
  • 5,414
  • 4
  • 26
  • 33
patrys
  • 2,729
  • 17
  • 27
  • 2
    Also I should add that local laws usually provide you with explicit points to quantize at. – patrys Sep 26 '11 at 20:28
  • 6
    +1 This is the best advice. Setting precision globally to 2 decimal places is a nonsense. Next step: "given a unit price (4 decimal places) and an amount of money (2 dp), determine how many units (3 dp) that will buy". Also consider that you might be working on a software package which you hope will be used by multiple customers in multiple jurisdictions -- the number of dp in a unit price and a qty of units had better be configurable. – John Machin Sep 26 '11 at 20:48
22

Falsehoods programmers believe about money:

  • Monetary values can be stored or represented as a floating point.
  • All currencies have a decimal precision of 2.
  • All ISO 4217 defined currencies have a decimal precision.
  • All currencies are defined in ISO 4217.
  • Gold is not a currency.
  • My system will never have to handle obscure currencies with more than 2 decimal places.
  • Floating point values are OK if the monetary value of transactions is "small".
  • A system will always handle the same currency (therefore we do not persist the currency, only the monetary value).
  • Storing monetary values as signed long integers will make them easier to work with, just multiply them by 100 after all arithmetic is done.
  • Customers will never complain about my rounding methods.
  • When I convert my application from language X to language Y, I don't have to verify if the rounding behavior is the same.
  • On exchanging currency A for currency B, the exchange rate becomes irrelevant after the transaction.
Cochise Ruhulessin
  • 1,001
  • 1
  • 11
  • 15
22

The accepted answer is mostly correct, except for the constant to use for the rounding operation. You should use ROUND_HALF_UP instead of ROUND_05UP for currency operations. According to the docs:

decimal.ROUND_HALF_UP

    Round to nearest with ties going away from zero.

decimal.ROUND_05UP

    Round away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise round towards zero.

Using ROUND_05UP would only round up (for positive numbers) if the number in the hundredths place was a 5 or 0, which isn't correct for currency math.

Here are some examples:

>>> from decimal import Decimal, ROUND_05UP, ROUND_HALF_UP
>>> cents = Decimal('0.01')
>>> Decimal('1.995').quantize(cents, ROUND_HALF_UP)
Decimal('2.00')  # Correct
>>> Decimal('1.995').quantize(cents, ROUND_05UP)
Decimal('1.99')  # Incorrect
>>> Decimal('1.001').quantize(cents, ROUND_HALF_UP)
Decimal('1.00')  # Correct
>>> Decimal('1.001').quantize(cents, ROUND_05UP)
Decimal('1.01')  # Incorrect
Community
  • 1
  • 1
keithb
  • 1,940
  • 16
  • 15
  • 1
    I'm working on an accounting script. I'm prolly not using Decimal at all the right spots, but I discovered testing the sum of multiple lines of Debits and Credits in a single transaction (which must always equal zero) rounding ridiculous small numbers (1.8474111129762605e-13), ROUND_05UP always gives you a penny. – xtian Oct 21 '18 at 20:30
2

One way to solve this is to store money values in cents as integers, and only convert to decimal representation when printing values. This is called fixed point arithmetic.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
  • 2
    There is value in understanding the concepts behind the Decimal module. – Greg Hewgill Sep 26 '11 at 20:06
  • 6
    I will not down-vote but will mention instead that this leads to various other problems. For example sooner or later you will have to multiply your prices by other fractions like quantities or exchange rates. That will result in having to truncate extra trailing digits to move the decimal point back to its proper position. – patrys Sep 26 '11 at 20:48
  • 2
    @patrys: That's a good point. Of course, one would have the same problems with the Decimal module if the precision were set to exactly two places. – Greg Hewgill Sep 26 '11 at 21:00
0
>>> decimal.getcontext().prec = 2
>>> d = decimal.Decimal('2.40')
>>> d/17
Decimal('0.14')

You just have to set the precision to 2 (the first line) and them everything will use no more than 2 decimal places

Just for comparison:

>>> 2.4 / 17
0.1411764705882353
JBernardo
  • 32,262
  • 10
  • 90
  • 115
  • what happens if the original number is just 2 and i want to be displayed as 2.00? – Musaab Sep 26 '11 at 19:53
  • @Musaab For printing, you should use [string formatting](http://docs.python.org/library/string.html#format-string-syntax) – JBernardo Sep 26 '11 at 19:57
  • 2
    You should not globally change the behavior of `Decimal` unless you understand the possible outcome and have tools to measure it (write a lot of unit tests, unit tests are always good!). Fiddling with global settings of a library on one end of your application can easily break a seemingly unrelated thing in another part of the project. If you are lucky enough, you might also change the behavior of third-party modules that you use and that happen to use `Decimal` internally. – patrys Sep 26 '11 at 20:41
  • 1
    There will be unexpected behaviours that are not very "currency-like" if the precision is changed to 2. Precision does not mean "decimal places". Try `decimal.Decimal('9.99') + decimal.Decimal(0) == 10` with context `prec` set to 2. – jamesc Apr 08 '15 at 15:36