In earlier posts, we discussed how the decimal module can help mitigate the precision limitations inherent in Python’s float type.
However, even the Decimal type cannot provide infinite precision. This can be problematic when working with rational numbers—those that can be expressed as the quotient of two integers—if their decimal representation is infinitely repeating. In such cases, it can be particularly useful to work with common fractions.
Common fractions are defined by a numerator and a denominator and can be created using the Fraction type from the standard library’s fractions module.
Representing infinite decimals with the float type can not only lead to precision issues but also cause subtle runtime errors that can be difficult to detect and fix. By using fractions ( Fractionobjects), such issues can often be resolved.
Let’s illustrate this with an example.
Imagine a scenario—such as a wedding—where a cake of a given weight must be divided precisely among guests. To model this, we assume that each slice’s weight is calculated as a fixed ratio of the original total cake weight.
Here’s a simple function that models this process:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
from fractions import Fraction def cut_cake(cake_weight, share_ratio): # cake stores the constant, original total weight, used to calculate fixed slice size. cake = cake_weight # grams slices = [] # grams - stores the weight of each slice remaining_cake = cake # Loop continues until the remaining amount is negligible (close to zero). # This tolerance check is necessary for float-based arithmetic. while not (abs(remaining_cake) < 1e-11): # Calculate the slice weight based on the original total cake weight slice_weight = cake * share_ratio # Store the slice weight slices.append(slice_weight) # Subtract the slice from the remaining cake remaining_cake = remaining_cake - slice_weight return sum(slices), remaining_cake cake_weight = 3000 # grams # Test 1: Share ratio specified as a float (1/171 results in repeating decimal) # Note: cake_weight is passed as an integer, but 1/171 is a float, forcing float math. total_slice_weight_float, leftover_float = cut_cake(cake_weight, 1/171) print(f'Float Test:') print(f'Total weight of slices: {total_slice_weight_float} g. Leftover: {leftover_float} g') # Result: Total weight of slices: 2999.9999999999995 g. Leftover: 6.373568339768099e-12 g print("-" * 30) # Test 2: Share ratio specified as a Fraction # Passing a Fraction object ensures that all internal calculations use fraction arithmetic. total_slice_weight_fraction, leftover_fraction = cut_cake(cake_weight, Fraction(1, 171)) print(f'Fraction Test:') print(f'Total weight of slices: {total_slice_weight_fraction} g. Leftover: {leftover_fraction} g') # Result: Total weight of slices: 3000 g. Leftover: 0 g # Note about the results: # The float calculation fails to reach a perfect zero due to the binary representation # of the repeating decimal 1/171, leading to a small but definite leftover. # The Fraction calculation maintains the exact rational representation (3000 * 1/171) # at every step, allowing it to sum up perfectly to the original 3000. |
The first argument of the function specifies the total cake weight in grams.
The second argument represents the share ratio, which is the reciprocal of the number of guests.
Inside the function, we repeatedly subtract a slice from the remaining cake until nothing is left. Since we know that floating-point arithmetic doesn’t always produce an exact zero, we stop the loop when the remaining amount is sufficiently close enough to zero, based on a chosen tolerance value.
For verification, the function returns both the sum of all slice weights and the remaining cake weight. As we can see from the test results, using Fraction produces exact values, whereas using float does not.
One might think we could improve accuracy simply by tightening the loop’s tolerance condition — say, changing it from 1e-11 to 1e-12 or smaller. But if we try that, the program enters an infinite loop.
That’s because the results of repeated subtractions stop changing after a certain point—the difference never becomes smaller than a specific minimum, which itself depends on the initial cake weight. So if the cake’s weight changes, a tolerance value that once worked might suddenly fail.
In this case, the only reliable and accurate solution is to use the Fraction type. The details of working with Fraction objects, along with additional interesting use cases—such as computing continued fractions—are covered in a dedicated chapter of the e-book Python Knowledge Building Step by Step.