Repository navigation
Description
round with non-zero digits is notoriously hard to implement correctly, and I think #1521 underestimates the difficulty. The following script produces a lot of counterexamples in RustPython but none in Python 3 (at least after 10M trials):
from random import random
trial = 0
while True:
trial += 1
if trial % 10000 == 0: print(trial)
num = random()
# produces a single integral digit, `.` and 52+ fractional digits.
# unlike str, `%f` retains fractional digits of the true value.
#
# note: while not specified, the implementation of `random.random`
# ensures that those fractional digits contain all significant digits.
# this is pretty important for determining a midpoint for Banker's rounding;
# if it's not the case, simply change the number of frac digits to 1100+.
num_str = '%.60f' % num
# TODO: RustPython doesn't yet have random.randint
digits = int(1 + random() * 16)
# same to `int(num_str * 10**digits)`, but exact
scaled = int(num_str[0] + num_str[2:2+digits], 10)
# same to `round(num_str * 10**digits)`, but exact
scaled_and_rounded = scaled
if num_str[2+digits] >= '5':
# Banker's rounding: when `num` is equally close to both rounded values, prefer even
is_midpoint = all(c == '0' for c in num_str[3+digits:])
if not is_midpoint or scaled % 2 != 0: scaled_and_rounded += 1
# simulate `round(num, digits)`
scaled_and_rounded_str = ('%0' + str(1+digits) + 'd') % scaled_and_rounded
rounded = float(scaled_and_rounded_str[0] + '.' + scaled_and_rounded_str[1:])
rounded_impl = round(num, digits)
if rounded != rounded_impl:
print(f'failed: round({num_str[:2+digits]}_{num_str[2+digits:5+digits]}..., {digits}) = expected {rounded}, got {rounded_impl}')Example output in RustPython:
failed: round(0.3965324291450832_427..., 16) = expected 0.3965324291450832, got 0.3965324291450833
failed: round(0.289032886419551_493..., 15) = expected 0.289032886419551, got 0.289032886419552
failed: round(0.3364255023393837_307..., 16) = expected 0.3364255023393837, got 0.3364255023393838
failed: round(0.1182629781749734_432..., 16) = expected 0.1182629781749734, got 0.1182629781749735
failed: round(0.2254041491428512_289..., 16) = expected 0.2254041491428512, got 0.2254041491428513
failed: round(0.781000838713878_486..., 15) = expected 0.781000838713878, got 0.781000838713879
It should be noted that the string representation of round(num, digits) doesn't need to be correctly rounded. In fact, it might not be possible at all. Python's implementation only tries to return a floating point number closest to the true rounded value and probably it is the best possible result when you have to return floating point numbers.
Implementation-wise I think there is no way easier than converting a number to the stringy value and back, as Python does. Note that the stringy value doesn't need to be an actual string, it just has to be a value that can be easily converted to decimal digits. (Rust has an unstable interface to those stringy values, but they are exposed just for tests.) As we have BigInts as a given, we probably should make use of them.
Activity
youknowone commentedon Jul 9, 2026
Fixed by #7761 (round_float_digits now rounds at the decimal level via dtoa-style format!("{:.*}") instead of binary multiply-round-divide); the issue's reproducer runs 1,000,000 trials with zero counterexamples, byte-matching CPython 3.14.