88

Python Decimal doesn't support being constructed from float; it expects that you have to convert float to a string first.

This is very inconvenient since standard string formatters for float require that you specify number of decimal places rather than significant places. So if you have a number that could have as many as 15 decimal places you need to format as Decimal("%.15f" % my_float), which will give you garbage at the 15th decimal place if you also have any significant digits before decimal (Decimal("%.15f" % 100000.3) == Decimal('100000.300000000002910')).

Can someone suggest a good way to convert from float to Decimal preserving value as the user has entered, perhaps limiting number of significant digits that can be supported?

Nickolay
  • 31,095
  • 13
  • 107
  • 185
Kozyarchuk
  • 21,049
  • 14
  • 40
  • 46
  • 4
    Note that in python 2.7 you don't have to cast to a string first, Decimal(0.1) is now valid which prints as Decimal('0.1000000000000000055511151231257827021181583404541015625') – Nick Craig-Wood Sep 09 '11 at 15:41

13 Answers13

80

Python <2.7

"%.15g" % f

Or in Python 3.0:

format(f, ".15g")

Python 2.7+, 3.2+

Just pass the float to Decimal constructor directly, like this:

from decimal import Decimal
Decimal(f)
pyjavo
  • 1,598
  • 2
  • 23
  • 41
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • 31
    Passing float directly to `Decimal` constructor introduces a rounding error. It's better to convert a float to a string before passing it to the constructor. E.g. `Decimal(0.30000000000000004)` results in `Decimal('0.3000000000000000444089209850062616169452667236328125')`, and `Decimal(str(0.30000000000000004))` results in `Decimal('0.30000000000000004')`. – Aliaksandr Adzinets Mar 17 '20 at 14:00
  • 2
    @AliaksandrAdzinets: it depends on what you want to achieve. To get the value corresponding to the [float](https://en.wikipedia.org/wiki/Double-precision_floating-point_format#IEEE_754_double-precision_binary_floating-point_format:_binary64) (`0.30000000000000004 .as_integer_ratio()` -- the internal representation could be think of as binary numbers written in scientific notation), pass float directly. To get (naive) "what you see is what you get" decimal ratio: `30000000000000004/100000000000000000` , pass string. https://docs.python.org/3/tutorial/floatingpoint.html – jfs Mar 19 '20 at 18:46
  • 7
    @AliaksandrAdzinets: to be clear: it is NOT a rounding error. a float `0.30000000000000004` and a number represented by `'0.30000000000000004'` are different (because the later can't be represented exactly as a float) i.e., Decimal does NOT introduce the error here, it is the opposite `str()` changes float's value: it just happens that the error that `str()` produces might be desirable for a naive view on floats – jfs Apr 16 '20 at 15:33
  • 1
    Note that `format(0.012345, ".2g") == 0.012` - three dps, whereas `format(0.12345, ".2g") == 0.12`. The `f` formatter should be used: `format(0.012345, ".2f") == 0.01` – Chris May 27 '20 at 14:49
  • 1
    @Chris: no. `f` would be wrong here. Try `1.23e-20` – jfs May 27 '20 at 18:36
  • This answer is really confusing, as the first version using [%g](https://stackoverflow.com/q/41840613/1026) behaves differently from the second version: the former attempts to round the float to 15 significant digits, while the second one doesn't. – Nickolay Nov 05 '21 at 05:06
  • @Nickolay: please read the comments above (the 1st variant is "what you see is what you get", the 2nd one likely to get the exact representation that may be surprising for some people – jfs Nov 05 '21 at 10:55
  • @jfs please don't assume that I haven't read the comments; I have, and I believe the answer is confusing for the reasons stated. (And not even the comments explained what ".15g" was trying to do.) Anyways, seems that people on modern python versions should just use `Decimal(repr(my_float))` if they absolutely can't fix their code to avoid floats in the first place. – Nickolay Nov 05 '21 at 22:05
  • @Nickolay: no. There is no one preferred way that is suitable in all cases (what to choose 1st or 2nd depends on context and if somebody doesn't care they can use either) – jfs Nov 06 '21 at 08:13
33

I suggest this

>>> a = 2.111111
>>> a
2.1111110000000002
>>> str(a)
'2.111111'
>>> decimal.Decimal(str(a))
Decimal('2.111111')
vincent wen
  • 413
  • 6
  • 9
  • 4
    This approach gives the most sensible result. Constructing a Decimal directly off the float as in Sebastian's answer can give surprising results in Python 2.7. Well, not surprising at all if you've played with floats before... – Nick Chammas Feb 28 '14 at 22:32
  • 2
    One note with this (as raised in comment for answer using `repr()`) is that `str()` here will limit to 17 decimal places, e.g. `Decimal(str(0.12345678901234567890))` results in `Decimal('0.12345678901234568')`. – Gary Mar 23 '19 at 15:26
  • Well, that doesn't work with: 8.9*0.7 --> >>> str(8.9*0.7) '6.2299999999999995' – Denny Weinberg Oct 13 '22 at 09:38
32

You said in your question:

Can someone suggest a good way to convert from float to Decimal preserving value as the user has entered

But every time the user enters a value, it is entered as a string, not as a float. You are converting it to a float somewhere. Convert it to a Decimal directly instead and no precision will be lost.

nosklo
  • 217,122
  • 57
  • 293
  • 297
  • Unfortunately it's stored as float in the DB and I can't change the schema – Kozyarchuk Nov 25 '08 at 04:10
  • 15
    Then you have already lost “the value as the user has entered” and you can't get it back. All you can do is apply an arbitrary rounding and hope. Python's treatment of Decimal correctly brings this to your attention so you understand the problem now instead of getting a weird bug later. – bobince Nov 25 '08 at 12:03
  • 2
    @Kozyarchuk: I know you said that you can't change the schema, but you may want to think about changing it to a decimal type (at some point), because this can become more of a headache in the future. – Jeremy Cantrell Nov 25 '08 at 22:24
7

you can convert and than quantize to keep 5 digits after comma via

Decimal(float).quantize(Decimal("1.00000"))
Ryabchenko Alexander
  • 10,057
  • 7
  • 56
  • 88
5

Python does support Decimal creation from a float. You just cast it as a string first. But the precision loss doesn't occur with string conversion. The float you are converting doesn't have that kind of precision in the first place. (Otherwise you wouldn't need Decimal)

I think the confusion here is that we can create float literals in decimal format, but as soon as the interpreter consumes that literal the inner representation becomes a floating point number.

muhuk
  • 15,777
  • 9
  • 59
  • 98
4

The "official" string representation of a float is given by the repr() built-in:

>>> repr(1.5)
'1.5'
>>> repr(12345.678901234567890123456789)
'12345.678901234567'

You can use repr() instead of a formatted string, the result won't contain any unnecessary garbage.

Federico A. Ramponi
  • 46,145
  • 29
  • 109
  • 133
  • repr returns 17 significant places, but there are often errors in 16th and 17th significant place. So the following does not work. from decimal import Decimal start = Decimal('500.123456789016') assert start == Decimal(repr(float(start))), "%s != %s " % (start, Decimal(repr(float(start)))) – Kozyarchuk Nov 25 '08 at 03:09
  • 4
    @Kozyarchuk: This is not an issue with the number of significant digits. It's a problem with binary representation of decimal numbers. Your example fails for 0.6, too. – davidavr Nov 25 '08 at 04:07
  • 1
    `repr` returns the shortest representation that results in the same IEEE-754 double. – Pedro Werneck Mar 06 '19 at 03:27
  • I should note that the '500.123456789016' testcase works fine in the modern versions of Python (possibly thanks to https://bugs.python.org/issue1580 ?), so `Decimal(repr(my_float))` seems to be the way to go if you absolutely can't avoid the float in the first place. – Nickolay Nov 05 '21 at 05:45
4

I've come across the the same problem / question today and I'm not completely satisfied with any of the answers given so far. The core of the question seems to be:

Can someone suggest a good way to convert from float to Decimal [...] perhaps limiting number of significant digits that can be supported?

Short answer / solution: Yes.

def ftod(val, prec = 15):
    return Decimal(val).quantize(Decimal(10)**-prec)

Long Answer:

As nosklo pointed out it is not possible to preserve the input of the user after it has been converted to float. It is possible though to round that value with a reasonable precision and convert it into Decimal.

In my case I only need 2 to 4 digits after the separator, but they need to be accurate. Let's consider the classic 0.1 + 0.2 == 0.3 check.

>>> 0.1 + 0.2 == 0.3
False

Now let's do this with conversion to decimal (complete example):

>>> from decimal import Decimal
>>> def ftod(val, prec = 15):   # float to Decimal
...     return Decimal(val).quantize(Decimal(10)**-prec)
... 
>>> ftod(0.1) + ftod(0.2) == ftod(0.3)
True

The answer by Ryabchenko Alexander was really helpful for me. It only lacks a way to dynamically set the precision – a feature I want (and maybe also need). The Decimal documentation FAQ gives an example on how to construct the required argument string for quantize():

>>> Decimal(10)**-4
Decimal('0.0001')

Here's how the numbers look like printed with 18 digits after the separator (coming from C programming I like the fancy python expressions):

>>> for x in [0.1, 0.2, 0.3, ftod(0.1), ftod(0.2), ftod(0.3)]:
...     print("{:8} {:.18f}".format(type(x).__name__+":", x))
... 
float:   0.100000000000000006
float:   0.200000000000000011
float:   0.299999999999999989
Decimal: 0.100000000000000000
Decimal: 0.200000000000000000
Decimal: 0.300000000000000000

And last I want to know for which precision the comparision still works:

>>> for p in [15, 16, 17]:
...     print("Rounding precision: {}. Check  0.1 + 0.2 == 0.3  is {}".format(p,
...         ftod(0.1, p) + ftod(0.2, p) == ftod(0.3, p)))
... 
Rounding precision: 15. Check  0.1 + 0.2 == 0.3  is True
Rounding precision: 16. Check  0.1 + 0.2 == 0.3  is True
Rounding precision: 17. Check  0.1 + 0.2 == 0.3  is False

15 seems to be a good default for maximum precision. That should work on most systems. If you need more info, try:

>>> import sys
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)

With float having 53 bits mantissa on my system, I calculated the number of decimal digits:

>>> import math
>>> math.log10(2**53)
15.954589770191003

Which tells me with 53 bits we get almost 16 digits. So 15 ist fine for the precision value and should always work. 16 is error-prone and 17 definitly causes trouble (as seen above).

Anyway ... in my specific case I only need 2 to 4 digits of precision, but as a perfectionist I enjoyed investigating this :-)

Any suggestions / improvements / complaints are welcome.

ChristophK
  • 733
  • 7
  • 20
2

When you say "preserving value as the user has entered", why not just store the user-entered value as a string, and pass that to the Decimal constructor?

Paul Fisher
  • 9,618
  • 5
  • 37
  • 53
2

The main answer is slightly misleading. The g format ignores any leading zeroes after the decimal point, so format(0.012345, ".2g") returns 0.012 - three decimal places. If you need a hard limit on the number of decimal places, use the f formatter: format(0.012345, ".2f") == 0.01

Chris
  • 5,664
  • 6
  • 44
  • 55
1

The "right" way to do this was documented in 1990 by Steele and White's and Clinger's PLDI 1990 papers.

You might also look at this SO discussion about Python Decimal, including my suggestion to try using something like frap to rationalize a float.

Community
  • 1
  • 1
Doug Currie
  • 40,708
  • 1
  • 95
  • 119
  • 1
    It appears that a similar algorithm [was later implemented in Python 3](https://bugs.python.org/issue1580), so `Decimal(repr(float_value))`, as suggested in another answer, works fine today. – Nickolay Nov 05 '21 at 05:44
0

You can use JSON to accomplish it

import json
from decimal import Decimal

float_value = 123456.2365
decimal_value = json.loads(json.dumps(float_value), parse_float=Decimal)
Deep Patel
  • 619
  • 7
  • 8
  • 1
    This does not seem right. JSON is used as data interchange format and all the code will do is convert float to string and then load that string as Decimal. Downvoted. – dotz Oct 27 '21 at 23:50
  • Yet it reminds us that we got the float by parsing JSON in the first place, there's a way to sidestep this whole float->decimal conversion altogether. Thanks! – Nickolay Nov 05 '21 at 05:38
0

Inspired by this answer I found a workaround that allows to shorten the construction of a Decimal from a float bypassing (only apparently) the string step:

import decimal
class DecimalBuilder(float):
    def __or__(self, a):
         return decimal.Decimal(str(a))

>>> d = DecimalBuilder()
>>> x = d|0.1
>>> y = d|0.2
>>> x + y # works as desired
Decimal('0.3')
>>> d|0.1 + d|0.2 # does not work as desired, needs parenthesis
TypeError: unsupported operand type(s) for |: 'decimal.Decimal' and 'float'
>>> (d|0.1) + (d|0.2) # works as desired
Decimal('0.3')

It's a workaround but it surely allows savings in code typing and it's very readable.

mmj
  • 5,514
  • 2
  • 44
  • 51
-1

The question is based on the wrong assertion that:

'Python Decimal doesn't support being constructed from float'.

In python3, Decimal class can do it as:

from decimal import *
getcontext().prec = 128 #high precision set
print(Decimal(100000.3))

A: 100000.300000000002910383045673370361328125 #SUCCESS (over 64 bit precision)

That's the right value with all decimals included, and so:

'there is no garbage after 15th decimal place ...'

You can verify on line with a IEEE754 converter like:

https://www.binaryconvert.com/convert_double.html

A: Most accurate representation = 1.00000300000000002910383045673E5 (64 bit precision)

or directly in python3 :

print(f'{100000.3:.128f}'.strip('0'))

A: 100000.300000000002910383045673370361328125

Preserving value as the user has entered, it's made with string conversion as:

Decimal(str(100000.3))

A: 100000.3

giocip
  • 1
  • 3