The loss is unavoidable. Floats have no more than 64 bits of
information, and that includes 1 bit for the sign. But Fractions are
limited only by the amount of memory.
x = 1 - Fraction(1, 2)**1000000
is a fraction where both the numerator and denominator have more than
300,000 digits each. (That’s about a million bits each.)
You can’t fit 20 pounds of potatoes in a 10 pound sack, and you can’t
fit two millions bits in a 64 bit float
For every finite float, there is more than one possible numerator/
denominator pair that would be rounded to that float, even if you count
only pairs in simplified form.
(Strictly speaking, there are an infinite number of such numerator/
denominator pairs. But most of them involve ludicrously big values.)
Let’s take the three adjacent floats:
0.49999999999999994 = 9007199254740991 / 18014398509481984
0.5 = 1/2
0.5000000000000001 = 4503599627370497 / 9007199254740992
So there are a whole lot of fractions that exist within the range
0.499…4 to 0.500…1 that have to be rounded to one of those three
values. For example:
>>> float(Fraction(9007199254740993, 18014398509481984))
In principle, one of your users might genuinely expect the exact
fraction 9007199254740993/18014398509481984 and be mad that it rounds to
1/2. But it isn’t very likely.
But if you really care about this, then just use Fractions all the way
through your calculations, and don’t allow any floats to creep into the
calculation. Make sure your users only enter values as numerator,
denominator pairs, and there will never be any data loss.
Or you could just use limit_denominator with a denominator of, say,
50000 or 100000 or so. Most people aren’t going to care about fractions
with larger denominators.