Skip to content

round(number, digits) is incorrect for non-zero digits #1554

Closed

Description

@lifthrasiir

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

  1. youknowone commented on Jul 9, 2026

    @youknowone
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-stdlibC-compatA discrepancy between RustPython and CPython

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions