Integer division

What is the result of this code: print(6 // 0.4)
I thought the answer is 15.0 but I am getting 14.0. Any explanation for this answer I will appreciate.

This is a really good question!

I think what’s going on is that when 0.4 is converted to binary floating point, you get a value a tiny bit bigger than 0.4:

In [40]: f"{0.4:.40f}"
Out[40]: '0.4000000000000000222044604925031308084726'

So 6 / 0.4 is a tiny bit smaller than 15.0, and thus gets truncated to 14.0

However, (and now I’m out of my depth with FP) – when you actually compute 6 / 0.4, you do get 15.0 (which is exactly representable in binary) as far as I can tell:

In [43]: f"{6 / 0.4:.60f}"
Out[43]: '15.000000000000000000000000000000000000000000000000000000000000'

So I don’t get why the rounding doesn’t occur the same way before the truncation.

I’m sure someone that gets FP better than I do can explain …

-CHB

3 Likes

If we say x = 6 and y = 0.4

Then x / y would be 15, but // is called floor division, so you get 14.0, but what you don’t see is the remainder, which as a ratio, is less than ‘0.4’. You can see both the quotient and the remainder by using a function called divmod():

divmod(x, y) = (14.0, 0.3999999999999997)

Perhaps a more convincing use case would be if you needed to return the number of days and hours for any given number of hours:

day = 24  # number of hours in a day
hours = 172  # number of given hours
days, hrs = divmod(hours, day)
print(f"{days} Days, {hrs} hrs")

print(hours / day)
print(hours // day)

Output:

7 Days, 4 hrs
7.166666666666667
7

Edit done for minor code correction.

2 Likes

There is no “integer division” in Python. We have floor division which does division, keeping the floor of the result.

In case you don’t know what floor and ceiling are in maths:

Floor division with floats is weird, it is much easier to understand it with only integer arguments. 60//4 is exactly 15, as we expect.

As usual, the weirdness is because floats are binary numbers, not decimal. The float 0.4 is not exactly equal to the decimal number 0.4, it is actually a tiny bit larger:

>>> "%.50f" % 0.4
'0.40000000000000002220446049250313080847263336181641'

So we’re actually dividing by something a tiny bit bigger than the decimal 0.4. And that makes a difference.

  • In real numbers, 6 divided by exactly 0.4 gives exactly 15.

  • But something a tiny bit bigger than 0.4 only goes 14 times into 6, plus a reminder.

We can work out the remainder:

>>> 6 % 0.4
0.3999999999999997

Look at the remainder: it is almost equal to 0.4, in fact it is the very next float just under 0.4:

>>> math.nextafter(0.4, 0.0)
0.39999999999999997

With true division, 6/0.4, we get:

  • 6 divided by something a tiny bit bigger than 0.4 gives something a tiny bit less than 15;
  • but in this case, the division rounds up to 15.0 exactly.

If you are thinking this is all terribly complicated, you are right.

1 Like

What still confuses me here is this:

In [73]: f"{0.4:.40f}"
Out[73]: '0.4000000000000000222044604925031308084726'

OK, so the binary representation of the the literal 0.4 is a tiny bit larger than the real number 0.4 – got it. So then 6.0 / 0.4 would be expected to be a tiny bit smaller than 15.0:

In [75]: 6.0 / 0.4
Out[75]: 15.0

but a tiny bit smaller (as pointed out, as close as you can get in 64 bit float) would be displayed by python as a rounded 15.0.

However:

In [77]: f"{6.0 / 0.4:.40f}"
Out[77]: '15.0000000000000000000000000000000000000000'

so it seems to actually be 15.0, and exactly the same as what the literal 15.0 creates:

In [82]: (6.0 / 0.4) == 15.0
Out[82]: True

IIUC, small integers are exactly represented in FP – so it seems when we do the division we get exactly 15. So there seems to be some sort of rounding that’s going on when we do the plain division that’s not being done before the floor is taken in floor division – and that has me confused.

NOTE: the days, hours example reminded me of code I wrote years ago that converted latitude and longitude from decimal (float) values to degrees, minutes, seconds. The simple way, using % and the like, for certain values would result in things like:

9 degrees, 23 minutes 60 seconds
when that should be 24 minutes, zero seconds.

Sorry I don’t remember the particulars at the moment, but for certain values, you had the issue that the FP value was a tiny bit under 60, so it didn’t add another minute,but then rounded up to 60 for display.

I only caught it because I had arbitrarily chosen a number in a test that triggered the issue.

1 Like

Yes, integers with absolute value <= 2**53 are represented exactly in IEEE-754 double precision (CPython’s float type).

6.0 / 0.4 is exactly 15.0 because hardware division rounds it up to 15. 15.0 is the closest representable value to the infinitely precise result.

But Python’s floor division doesn’t care about that - it’s rounding down to the nearest integer in the direction of minus infinity. Hardware isn’t doing it - CPython is working hard to give you the true floor. To infinite precision, 6 divided by the float approximation of 0.4 is a little less than 15.0, so flooring that gives 14.0.

The lovely mpmath module lets you investigate things like this with ease, doing IEEE-style binary arithmetic with user-settable precision:

>>> import mpmath
>>> print(mpmath.mp)
Mpmath settings:
  mp.prec = 53                [default: 53]  # by default, 53 bits, same as a 754 double
  mp.dps = 15                 [default: 15]
  mp.trap_complex = False     [default: False]
>>> mpmath.mp.prec = 80  # boost precision to 80 bits
>>> print(mpmath.mp)
Mpmath settings:
  mp.prec = 80                [default: 53]  # we changed this
  mp.dps = 23                 [default: 15]
  mp.trap_complex = False     [default: False]
>>> mpmath.mpf(0.4) # the number in question
mpf('0.40000000000000002220446049')
>>> 6.0 / mpmath.mpf(0.4)  # showing that the infinitely precise result is < 15
mpf('14.999999999999999167332732')
>>> mpmath.mp.prec = 53 # but that rounds up to 15 under 53-bit precision
>>> 6.0 / mpmath.mpf(0.4)
mpf('15.0')
2 Likes

Correct, small integers are all exact in 64-bit floating point, but that’s not why 6.0/0.4 evaluates to exactly 15.0.

Under IEEE-754 maths, calculations are required to be correctly rounded. Anyone who is old enough to remember computer arithmetic before IEEE-754 can probably tell you many horror stories, such as

  • x != y but x - y = 0.0
  • x == y but x/y != 1.0

These abominations are impossible in IEEE-754 maths. But I digress.

I’m not an expert here, but my understanding is that arithmetic in IEEE-754 must use an extra three bits. So when we divide two 64-bit floats, the calculation actually uses 67 bits for the calculation, and then round the result back down to 64 when done.

When we calculate 6.0/0.4 we’re actually computing

6.0/(0.4 + d)

for some tiny value d, which works out as 15.0 - e for some other tiny value e. In this specific case, it just so happens that the difference between the true value 15.0 and the computed value 15.0 - e is small enough that when the three guard digits are rounded off, the result ends up being the true value 15.0.

But that isn’t guaranteed to always happen with every division:

(6*7)/(0.4*7)  # Mathematically exact result = 15.0
--> but returns 14.999999999999998

IEEE-754 semantics guarantee that when you divide two binary numbers, the result is always correctly rounded in binary. That doesn’t necessarily mean that the result looks correct in decimal, but sometimes we get lucky:

  • the exact value 6.0
  • when divided by the inexact value 0.4
  • gives a value slightly less than 15.0 when using 3 extra bits
  • which when rounded back to 64 bits ends up being the exact value 15.0
  • and multitudes rejoice.

Whereas the alternate calculation ends up rounding down to something a tad under the mathematically true value.

1 Like

Thanks Tim, that does it explain it, though to my naive eye, python’s floor division is not doing the right (or the best) thing here – the results woul be less surprising, if not more correct if:

a // b gave the same result as math.floor( a / b), which it is does not:

In [137]: 6 // 0.4 == math.floor(6 / 0.4)
Out[137]: False

Also in my naive view, I’m surprised // isn’t in fact implemented that way :slight_smile:

As far as I could tell with a quick search IEEE 754 doesn’t define “floor division” – I wonder what it would specify if it did?

NOTE: before floor division was defined in Python, I imagine folks would have done math.floor(a / b) – so an incompatibility was introduced.

In fact, if you use integers too large to be exactly represented by floats, then integer division IS different than floor division, which strikes me as sub-optimal:

In [148]: a
Out[148]: 60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

In [149]: b
Out[149]: 4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

In [150]: a / b
Out[150]: 15.0

In [151]: float(a) // float(b)
Out[151]: 14.0

Granted, for practical purposes, unlikely to come up, but still.

A better correlation would be that a // b should be the same as divmod(a, b)[0], that a % b should be the same as divmod(a, b)[1], and then that divmod is defined using this rule:

q, r = divmod(a, b)
assert q * b + r == a

as is stated in divmod’s docstring. As far as I know, this should be true of all numeric types. (Sadly, I can’t use divmod with strings, even if I subclass str such that x // y <=> x.split(y) - it still objects to the objects I’m feeding it. Awwww.)

The downside is, it’s notably harder to then explain floor division on its own; you have to simultaneously explain modulo.

A fully compliant 754 implementation needs to support the 4 distinct rounding modes defined by that standard. “To minus infinity” rounding is what it calls what we’re calling “floor” here. Its “to plus infinity” rounding is what we call “ceiling”. But core Python does not support any way to get at your hardware’s rounding mode setting. If it did, and you asked for “to minus infinity” rounding, your hardware division would also return a result less than 15.0 for 6 / 0.4.

>>> import mpmath
>>> mpmath.fdiv(6.0, 0.4)
mpf('15.0')
>>> mpmath.fdiv(6.0, 0.4, rounding='floor')
mpf('14.999999999999998')

Essentially all 754 operations are defined to return the infinitely precise result subject to one rounding at the end. There is no argument to be made about the “correct” result for 6 / 0.4. It’s precisely defined for each rounding mode.

A more fruitful question would be to wonder why Python supports “floor division” for floats at all. It’s very useful for ints, but ints have many pleasant mathematical properties floats lack. In a similar vein, the closely related definition for integer % results isn’t really what floats want either.

So it goes. A great many languages are seduced by the observation that, mathematically, the integers are a subset of the reals. That’s where “numeric tower” wishful thinking comes from. But machine floats are very far from being “the reals”. Fixed-precision machine floats are in fact a strangely lumpy finite subset of the rationals, augmented with weird :wink: “infinity” and “NaN” special cases.

Floor division was part of Python from day #1, so there is no “before”. However, for floats, to get at it you needed to spell it at first as divmod(x, y)[0].

Nothing about the meaning of that fragment has changed. / has always meant “true division” for floats. Although I’m lying a bit: math.floor(), until quite recently, returned a float instead of an int, mirroring what the C libm floor() does. Changing that was not, IMO, a good idea either.

I think you are surprised because you can’t get past the fact that the literal 0.4 looks like the exact decimal fraction 4 over 10, and so you think 6/0.4 is exactly correct and 6//4 has rounding error.

This is where the convenience of being able to enter and display binary floats in decimal bites us on the derrière and gives us the wrong intuitions about floats. It makes us surprised about things we shouldn’t be surprised about, and take for granted results which should be astonishing.

Instead of using the literal 0.4, let’s suppose your divisor was the result of some long, complicated computation, where you have no expectation that it would end up exactly one fifteenth of 6:

Here is an example of such a complicated computation, which is totally not contrived :wink:

x = 7205759403792794.0 * 5.551115123125783e-17

If we compute 6//x without first inspecting the value of x, and get 14.0, we’re not surprised in the least. We had to get some whole number, and 14.0 is no more or less surprising than any other.

If we now inspect the value of x to full precision, we see something like 0.40000…7263336181640625 and we’re hardly surprised that the number we computed is a “weird decimal”, nearly all numbers are “weird” in the sense of having a bunch of digits with no obvious pattern.

The only strange thing about this x is that there’s a bunch of 0s in a row in the middle of the number, but that’s not that surprising. Lots of numbers have a bunch of zeroes in them.

So now let’s take that “weird decimal” x, and divide it into 6. It would be some coincidence if it happened to divide exactly into 6 and give a whole number result, right?

In fact, if we look at the long tail of digits in the exact decimal representation …7263336181640625 it would be astonishing if it divided exactly, we ought to get something like 14.9999999…987 or 15.000000…132 or something.

And yet, as if by magic, the rounding error in 6/x disappears and leaves us with exactly 15.0.

I don’t know about anyone else, but I find it far more amazing that binary floating point arithmetic ever gives the mathematically correct values I expect in decimal than the fact that sometimes it doesn’t.

1 Like

Well, no, I’m not – I thought I made it clear that I was surprised because I expected “floor division” to be the same as taking the floor after dividing – i.e. floor(a / b) – and it’s not, which Tim explained quite nicely.

I think Tim’s point about maybe floor division being a tricky idea for floats is a good one.

For that matter, I’ve been surprised more than once that a_float // a_float returns a float – isn’t it always supposed to be an integral value?

if floor(a_float) returns an int, then why not floor division?

(Tim argues that floor() shouldn’t return and int – which would at least be consistent)

I’m particularly surprised, because in_int // an_int does return an int, and the whole thing was created to clear up the surprises that we got when the result of an operation depended on the type (rather than the value) of the operators.

This has practical considerations – I’ve often used // to compute an index – having to wrap a int() around the result is a bit annoying.

By the way, I may have been wrong, I think folks would expect that:

a // b would be the same as int(a / b) – which it also doesn’t.

Oh well.