Literals for Decimal and Fraction

It is not just that float to decimal conversion is exact. The Decimal constructor is exact for all possible inputs be they int, float, string or the tuple of ints representation. This is important because part of the idea of the decimal module is to have precise control over how things like rounding are managed. You want the constructor which is not a context method to be exact so that you can then use a context to control any subsequent rounding. That way you can guarantee to round up for example or trap the inexact signal.

The proper solution is really to have a way of spelling numbers exactly rather than trying to recover after a lossy conversion. That is the benefit that decimal and fraction literals would provide and without them strings are the next best thing.

1 Like

The only problem I see here is the misconception “using decimal literals”. All confusion disappears simply by learning not to make that false assumption. In principle, if there are no decimal literals, one shouldn’t be making that assumption anyway.

Regarding the parenthesized statement, older versions of Python used to display a (sometimes) different representation of the float. So, another thing to learn is that the float-literal to float correspondence is many to one. The assumption that one literal shows “not exactness” more than other is also another unwarranted assumption.

Well, I’m not fan of rewriting everything in C.

The point was - yes, we can add literals for Fractions without this. But just as Paul - I don’t see big benefits from this shortcut. F(1,2) is not too much worse than 1/2f and definitely expects less from the reader.

That’s a kind of changes, on which I would buy decimal literals:)

But changing meaning of float’s to be decimal float’s - is something for Python 4.0. Probably, this ship sailed.

I don’t think it’s a real problem. Binary floats (assuming system, implementing the C Annex F) have much more stuff than exposed by Python’s float type API.

I’ll preface this by saying that I agree that Oscar’s point about exactness in the constructor conflicts with (and almost certainly supersedes) a goal of making the constructor to be more user-friendly.

Curiously, my proposal is that the conversion would be inexact, but still lossless. Since Decimal has far more precision than float, there are many possible Decimal values that evaluate to the same float, for any given float. The difference would be that the Decimal intermediate would be chosen to, e.g., use the shortest precision needed to guarantee a unique Decimal for every float, rather than the current choice of using as much precision as required to exactly represent the provided float.

(It’s also worth noting that Decimal → float is almost always lossy, unless the initial Decimal happens to be the exact value returned by the Decimal constructor.)

Naming is honestly the hard part here. I used D to provide brevity (consistent with the opening proposal to create Decimal literals), but this would not be an appropriate name to include in the standard library. If we were to add a helper function to the decimal module, it would need to be sort enough to be handy, but not conflict or cause confusion with the existing uses of decimal and Decimal. Options I’d consider would be decimal.dec or, more verbosely, decimal.as_decimal. Does anyone else have other suggestions?

If there’s support for it, I’d be happy to propose adding such a recipe to the docs as a first step, but I think recipes are much less discoverable and have poorer UX relative to including a function in the decimal module.

1 Like

Sounds meaningless, sorry.

You are reinventing David Gay’s algorithm for finding the shortest floating point representation, that’s used by str() circa CPython 3.1. But you can explicitly construct such Decimal instance from a float f as Decimal(str(f)).

This decimal value, in general, will be not equal to f mathematically. It will be a different rational number. Why the hell this silent lossy conversion will be better than current approach, which just choose equal number? (BTW, all you save — just two characters: D(0.1) vs D("0.1").)

Current Decimal(float) form is useful for people to get exact decimal fraction, equal to the given binary floating-point number. It will be odd if different Decimal constructors (i.e. Decimal() and Decimal.from_float()) will interpret same input differently.

That’s why there is no implicit conversion from Decimal’s in the float constructor.

What about rich comparisons? Are you sure, that < and > will return something “expected” (from the school) after you “fix” equality?

Yup, you’re right that changing the behavior of how the Decimal constructor handles floats is a bad idea. Consider it dropped.

Since the outcome of this thread appears to be a “no” on introducing Decimal literals to the language. As I understood it, the primary motivation is to improve the ergonomics of creating Decimal objects from literals (particularly the footgun that Decimal(0.1) != Decimal("0.1")). I was hoping to explore other avenues to improve those ergonomics. But I am probably out of my depth and will bow out from that process.

2 extra symbols does really really matter. It’s friction every time you type a number.

I think the constuctor I’d want that’s closest to being compatible with Python is
Decimal(0,1)==Decimal("0.1"). If it’s in the standard library we could even push for linters/formatters to format it as Decimal(0,1) instead of Decimal(0, 1).

Decimal(0,1) is currently a TypeError, so this might be backwards compatible.

Decimal("0.001") would still give issues. But you’d provide a better solution to 9/10 cases.

-1, that’s a horrible idea.

1 Like

Could you be a bit more specific why it’s horrible?

It mirrors fractions.Fraction(1, 3) which works quite well, and there are plenty of countries around the world where 0,1 is the default way to write numbers with decimals.

It’s abuse of notation, most programming languages don’t use comma’s for decimal numbers.
And the fact that it doesn’t work in all cases is more proof that this is a bad idea.

Writing Decimal("1e-1") as Decimal(1, e=-1) seems more justifiable, but saves at most 1 character.

What happens if you write Decimal(0,0010) ?

Cell In[3], line 3
    Decimal(0, 0010)
               ^
SyntaxError: leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers

both the part before and after the comma have to be valid integers. Integers can’t have leading zeros. It’s going to crash immediately, and tell you precisely where you did something wrong. As errors go, that’s a pretty good case.

That’s a bit of a weird limitation. The part of a number after the decimal point often does NOT have to be a valid integer.

1 Like

I agree that the extra symbols matter. There really is a usability difference between D("1.1") and 1.1d.

The problem is that decimals simply aren’t useful enough to justify new syntax. They are a convenient example of a case where a good literal syntax matters, but they just aren’t used enough in practice.

I don’t think I’ve ever written or seen real-world code that uses decimals. And I doubt that would have been different if decimal literals were built in. My experience is just in one particular field, so the experience of others may vary, but I doubt the overall result will be much different - decimals simply aren’t used very often.

IMO, the only way we’d get a literal format for decimals is as a side-effect of some form of “user defined literals” feature, that gains its justification from being applicable to a number of different use cases, each of which might on their own be insufficient. But I don’t think that even that is likely - very few languages support user defined literals (C++ is the only mainstream one that I know of) suggesting that they aren’t as useful in practice as you might think.

1 Like

Agreed. That’s why I did not propose new syntax :wink: . Just Decimal(12,34), a syntax that works well for 90% of cases.

I’ve got real production code where decimals would have been useful. Specifically, in testing. I wanted to assert things were equal, but float rounding got in the way.
In the end I went with octal numbers. It’s a good enough solution.

But it doesn’t work in an useful way. As has been pointed out, it can’t represent 1.01.

The 90%[1] of cases you claim excludes many reasonable examples, and it does so in a pretty unintuitive (at least IMO) way.

Seriously, having to write D("1.01") rather than D(1.01) was enough to make you rewrite the logic of your production code? That seems rather extreme (and while I’m not disputing your claim, I don’t think it’s anywhere near typical enough to prove anything).


  1. That number could easily be wrong, see Benford’s Law ↩

2 Likes

It may very well be that the trade off makes it not worthwhile to introduce Decimal literals but I think it is important to see the problem more broadly than just using the Decimal type. In Python different numeric types can be mixed, converted, coerced, compared etc and ultimately numbers of all types when specified explicitly in code are created from literals e.g.:

>>> np.float64(1)
np.float64(1.0)
>>> sympy.Rational(0.5)
1/2
>>> sympy.Rational(0.1)
3602879701896397/36028797018963968
>>> mpmath.mp.dps = 50
>>> mpmath.mpf(0.1)
mpf('0.10000000000000000555111512312578270211815834045410156')
>>> mpmath.mpf('0.1')
mpf('0.10000000000000000000000000000000000000000000000000007')
>>> mpmath.mpf(1/3)
mpf('0.33333333333333331482961625624739099293947219848632813')
>>> mpmath.mpf(1)/3
mpf('0.33333333333333333333333333333333333333333333333333311')

The issue is not just about wanting to use the Decimal and Fraction types themselves but rather about being able to specify an exact numeric value as an input to any operation including the constructor of any other numeric type. Currently the best we have for this is to use string literals but we don’t want to allow strings to be coerced implicitly into numeric types so we need the explicit conversion (D(s)) everywhere.

3 Likes

So would an “unparsed number” literal be helpful? Something like 0u123.456 which produces a string literal "123.456"? That would provide a generic solution, which would be essentially a “drop in” solution (as numeric types typically already have some sort of “construct from string” operation, precisely because of the exactness issue).

The problem, of course, is that 0u123.456 is no simpler in any real sense than "123.456".

User perception is important - but is it that important? I honestly don’t know


4 Likes

Personally, I would use Fraction and Decimal way more often if there were literals for it.

Although I don’t often need these in production, but they are very useful when prototyping / doing analytical work.

Currently, I have preset imports for environments where I do this - from decimal import decimal as D; from fraction import fraction as F, but if there were literals for these, then it would both:

  1. be more convenient and less ambiguous than one-letter aliases in places where I do those
  2. I would use them more often for calculations when I just open REPL for some quick calculations. Now I mostly don’t bother and get satisfactory result from float.

But this is my personal case.

From POV of longer term consequences of such, I would guess that one positive is that it would accelerate progress for decimal & fractions in all areas, such as performance, features and compatibility with the rest.

So all in all I am ±0 - don’t have sufficient information for strong opinion, but from user’s perspective I would be pleased with this for sure.

E.g. To me this would be quite convenient (maybe not as pretty (initially clear) as having . and /, but unambiguous and avoids most of / some of (valid) criticism that I have seen in this thread):

print(1f2 + 1f2)
# Fraction(1, 1)

print(0d1 + 0d1)
# Decimal('0.2')
4 Likes

Given that 0x1 uses 0x as a prefix, having 0d mean 0. (but in decimal) would be very confusing. 0d0.1 seems far more likely to be acceptable. There’s no equivalent confusion with 1f2 but I think it would be considered too obscure in practice.