Python: poor printing of floating point

[edit on 2010-01-26: allegedly this is all fixed in Python 3.1. Please skip article if that’s okay with you]

In an attempt to dispell comments on the lines of “floating point doesn’t produce exact answers”, I should point out that I know:

• Floating point is tricky; and,
• 94.8 is not exactly representable as a floating point number.

Without delving into the arcane details of floating point let’s lay out some basics. Each floating point value is some exact real number, in fact a rational number. I’m ignoring NaNs and infinities because they’re not important for this discussion. There are only finitely many floating point values.

With regards to input and output of floating point numbers what is reasonable?

Reasonable input requirement

Every decimal number has some floating point number that is nearest (possibly two if it lies midway between two adjacent floating point numbers). It seems reasonable to me that when converting decimal numbers to floating point numbers (as Python does when it reads 94.8 in the program) it should pick the nearest floating point number to represent it internally (and if there are two such nearest, then pick one). (For very large magnitude numbers you might want to return infinity or throw an exception; doesn’t matter for this article.)

Reasonable output requirement

When converting a number from internal floating point format to decimal (as Python does when printing) it seems reasonable to print one of the decimal numbers whose nearest floating point number is the one we are printing. In other words we would like to print a number that when read according to the reasonable input requirement (above) we get the same internal floating point number. Note that there are infinitely many decimal numbers with a given nearest floating point number (because there are only finitely many floating point numbers); once you’ve printed enough decimal digits to zoom in on the desired floating point number you can keep on printing whatever digits you like, generating lots of different decimal numbers all of which share the same nearest floating point number.

Desirable output requirement

Consider the two number 94.799999999999997 and 94.8. By our reasonable output requirement above, they’re both good numbers to print out, but the shorter one is easier to read and think about for humans. It also satisfies an elegant principle: Don’t print out any more digits than you need to.

So a desirable requirement for printing out numbers is that we print out a number that meets the reasonable requirement, and is the shortest possible of all such numbers.

If you need a bit of persuading that shorter is better, then this argument might help you: Consider π (pi). An approximation to 18 decimal places is 3.141592653589793238. Now imagine you have written a Python program to compute π as accurately as it can. It prints 3.1415926535897931. That last digit is wrong. It’s not so much wrong as unnecessary, 3.141592653589793 represents the same floating point number:

>>> 3.141592653589793 == 3.1415926535897931
True

By printing that extra garbage digit Python is misleading you as to how many digits of precision it has.

Can we be desirable?

So how easy is it to generate reasonable output and desirable output? Well it turns out to be a bit trickier than you might think on first blush, and it requires bignums. On the other hand, this stuff was all sorted out about 30 years ago, and every reasonable machine is large enough to do bignum arithmetic without busting a gut. Unfortunately the gurus Steele and White didn’t publish the algorithm until 1990 (see [SW1990]). To quote them: “Why wasn’t it published earlier? It just didn’t seem like that big a deal; but we were wrong.” Boy were they wrong. Even 17 years after publication, modern implementations of modern languages running on modern hardware still get it wrong.

[Political rant: I’m inclined to think that one the contributory factors is that the article was published by the (“we love dead trees”) ACM. It just doesn’t seem helpful to lock all this knowledge away and charge for it. I happen to have a photocopied version of this article on paper (I guess that makes me lucky, and able to write this blog article), but in 2007 I can hardly believe that I have to resort to paper copies to do research.]

Bottom line: 94.8 should print out as 94.8.

Python’s excuse will no doubt be that it relies on the platform’s C library. C has an excuse. C targets tiny platforms where it would be ridiculous to suggest that the printing routines use 320 bytes of RAM to store intermediate computations (on a platform I recently had the pleasure to program, this would’ve been about 10% of the entire RAM). On the other hand any platform where I’m actually likely to use double I almost certainly have enough space for both for code and the bignum arithmetic it requires to get the right answer. Certainly, on my MacBook the C implementation has no excuse.

It’s no coincidence that Steele had a hand in writing the 1990 paper and also a hand in designing two of the languages that actually bother to say something sensible about converting internal numbers to external format. Scheme says that reading what you print will yield the same number, and this shall be done using the minimum number of digits. Java says “as many, but only as many, more digits as are needed to uniquely distinguish the argument value from adjacent values of type double”. Curiously, Common Lisp doesn’t seem to actually state anything like that, but possibly I just can’t find it. Common Lisp implementations, of course, get it right, because they’re invested with the spirit of Do The Right Thing. I had a look to see what Haskell said on the matter, but apart from noting with amusement that a literal number did not denote an internal number object I couldn’t find anything. ECMAScript also specifies the Right Thing, but then, Wikipedia reckons that Steele edited the first edition.

Python has automatic memory management, bignums, strings. It feels like a big language, abstracted and distant from the silicon. It’s time for Python to grow up and specify that repr for float returns a decimal string the mathematical value of which is closer to the argument than any other float, and it returns a string with the fewest number of digits.

The trickiest thing about Steele and White’s algorithm is the bignums. Python has bignums. It turns out to be very easy indeed to implement Steele and White’s algorithm in Python. We can then use this algorithm to create conversion routines in the style of ECMAScript or Java. And I have done so. The implementation isn’t very large, but it is too large to contain in the margins of this article. If you want the code then go to, yet another dead before it began sourceforge project, flopri.

Blind Alleys

Python has a module called fpformat which sounds like it might be the sort of place to find optimal floating point formatting routines, but its useless. “unneeded” as its own documentation avers. pprint is no good either; pretty, but no smarts.

Stop at any finite number of bits, and you get an approximation. This is why you see things like:
>>> 0.1
0.10000000000000001

As discussed above, 0.1 is a perfectly good approximation to the floating point value that 0.1 becomes on input, so approximation itself is not the reason that Python prints so many digits (don’t believe me, then try evaluating «0.1 == 0.10000000000000001»). The appendix goes on to recommend str if you want prettier output. I think this is the wrong fix for the problem; if Python used a different printing routine for repr then people wouldn’t be driven to str.

“You’ll see the same kind of thing in all languages that support your hardware’s floating-point arithmetic”. No I won’t. Java, ECMAScript, Lisp all get it right.

Hate to get all pedantic, but python prints just fine, it just doesn’t ‘repr’ the way you want. Your pi example is also a little wierd, because python (or really the processor’s floating-point representation) can’t create the value 3.141592653589793

Clearly f2 is the best representation of pi that we can get with IEEE 754 doubles. Equally clearly, both d1 and d2 should converted to f2 on reading (as should an infinity of other decimal numbers; everything between (f1+f2)/2 and (f2+f3)/2).

David is arguing that f2 should be written as d1, since d1’s representation is shorter than d2’s (or that of any of the other values which reads as f2). It doesn’t give any digits which are irrelevant to the underlying value. Such additional digits are misleading and unhelpful. repr(f2) could output the 50+ digit exact expansion which I give, but that would not be helpful. In just the same way, repr(94.8) should produce ‘94.8’.

@Grant: Thanks. I appreciate seeing the output on Windows (naturally I mess around on OS X).

Would you care to explain the extra ‘1’ digit at the end of math.pi? I know that 3.141592653589793 can’t be exactly represented as a floating point double, but then neither can 3.1415926535897931. In fact, these two numbers share the same floating point approximation:

>>> 3.141592653589793 == 3.1415926535897931
True

So why prefer the longer one over the shorter one?

[Oh: Whilst I was writing glorkspangle snuck in there with a response in the same vein but much more detail.]

Three points, David:
1. you don’t mention the other (adjacent) Steele&White paper. I infer from what you write that it has been successfully absorbed into the folk knowledge and the C library.
2. Both Steele and White papers, on paper, are lurking somewhere in a folder at the Cambridge office, adjacent to a paper copy of IEEE 754.
3. Harshing on the ACM: they’ll catch up, and for those of us with the right access they already have (I have an ACM digital library membership, all of everything forever as PDFs; RHSK and I have used it in the past for memory management papers). Also I think that one or both of the Steele&White papers is reproduced on a SIGPLAN commemorative CD-ROM (25 years of SIGPLAN? something like that) which I have floating about somewhere here in Manchester.

I was going to say, but never got round to, that I suspected that Python did something like “%.17g”. 17 decimal digits is enough to specify any double precision float (math.ceil(1+53/math.log(10,2)) == 17), so what Python does is give the C implementation chance to do the right thing. And evidently the C implementation on both Windows and OS X doesn’t quite do the right thing.

What it looks like the C implementation on OS X is doing is a rounding of the infinitely long answer. This produces a number that is reasonable, but contains misleading digits. glorkspangle’s first comment above illustrates this idea best.

@bosullvn: Thanks. Of course I was aware of this paper, but it seemed that everything you might want to know was in the Steele and White paper, so I’ve always been curious as to what’s in the Burger and Dybvig paper. I guess I should read it some day.

@glorkspangle: The companion paper on the input paper is by Clinger. I guess Steele has to have a day off sometimes. And no, I didn’t mention it. And yes I do assume it’s solved, I don’t know whether it is, but I was blithely assuming that Python solved the input problem, and I never noticed it be wrong. [edit: Turns out Python hasn’t solved the input problem, see Paul’s discovery below]

The conversion code can be found at http://haskell.org/onlinereport/numeric.html
It “specifies” the conversion in the same way as other Prelude functions do, namely that an implementation of a Prelude function must extensionally equal to the reference implementation in the report (i.e., must behave the same, but can be implemented differently).

Trying to write a little function to do this. I got as far as this:
>>> float_digits(math.pi)
‘3.141592653589793115997963468544185161590576171875000’
>>>
But it doesn’t work too well for exponents far from zero, and I’ve exhausted the budget.

@Paul: Amusingly cunning. The only thing I can really see that’s wrong about it is that it relies on the C implementation. Having decided to Do The Right Thing I figure you may as well get rid of that dependence. However, I think that’s a minor criticism. Your code is short, pragmatic, and given a decent enough C implementation it does the job. I like it. I’m pretty sure the C implementation on OS X is good enough.

Nitpick: small integers come out looking like int not float:

>>> toString(7.0)
‘7’

(so when you read them back in the type will be different). Depending on your Python philosophy, this is either tweakable or fine.

Or, if we don’t trust flopri (and I’m not entirely sure that I do yet), we can use glorkspangle’s method above. We can see from f’s hex representation that f is 0xfdc835c90e3a7 × 2**-1074 (f is subnormal). Multiplying top and bottom by 5**1074 we get: f = (0xfdc835c90e3a7 × 5**1074) × 10**-1074. Doing the bignum arithmetic in Python: