GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Round float at the decimal level to match CPython's _Py_dg_dtoa by changjoon-park · Pull Request #7761 · RustPython/RustPython

Conversation

@changjoon-park

@changjoon-park changjoon-park commented May 2, 2026 •
edited by coderabbitai Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Summary

CPython's float.__round__ (Objects/floatobject.c) routes through _Py_dg_dtoa and rounds at the decimal level. The previous round_float_digits multiplied by 10**ndigits and rounded at the IEEE 754 binary level, which diverges for values that aren't exactly representable.

>>> round(2.675, 2)
# CPython 3.14.4: 2.67
# RustPython main: 2.68 ❌

2.675 stores as 2.67499...; dtoa correctly rounds it down to 2.67, but (2.675 * 100.0).round() / 100.0 lands on 2.68 because the multiplication produces a phantom 267.5 tie that round-half-to-even snaps up.

Rust's {:.*} float formatting uses dtoa-style algorithms (Grisu3 + Dragon4 fallback) and matches CPython's _Py_dg_dtoa byte-for-byte. Replacing the multiply-then-round step with format!("{:.*}", ndigits, x).parse() removes the entire phantom-tie class for ndigits >= 0.

For ndigits < 0 the existing divide-then-round path is preserved because dividing typical inputs by 10**|ndigits| produces genuine half-integer ties rather than synthesizing them.

Verification (CPython 3.14.4)

Probe set Cases Match
Targeted half-tie probes (positive ndigits) 34 34/34
Negative ndigits regression 17 17/17
Special values (NaN, ±Inf, ±0.0, NDIGITS bounds) 12 12/12
No-arg round(x) (different code path) 10 10/10
Subnormal / denormal 6 6/6
Random fuzz (mantissa × ndigits ∈ {-5..50}) 4332 4332/4332

Tests unmasked

Lib/test/test_float.py::RoundTestCase:

  • test_matches_float_format — rounds against float(format(x, '.{n}f')) across many values; this is exactly what the fix does internally.
  • test_previous_round_bugs — explicit "round-half-even" cases (e.g. round(25.0, -1) == 20.0).

Test runs

cargo run --release -- -m test test_float test_builtin test_decimal \
    test_fractions test_complex test_math test_format test_long test_int \
    test_statistics test_re

1573 tests pass, 0 regressions. All 188 extra_tests/snippets/*.py pass under the CI feature set (stdlib,importlib,stdio,encodings,sqlite,ssl-rustls,host_env).

A regression block is added to extra_tests/snippets/builtin_round.py.

Summary by CodeRabbit

  • Bug Fixes

    • Improved rounding to more closely match CPython’s behavior: better handling of halfway ties (banker’s rounding), negative ndigits, signed zeros, and non-finite inputs.
  • Tests

    • Expanded rounding tests to cover banker's rounding, negative ndigits, exact-half cases, NaN/infinity behavior, signed zero, and out-of-range ndigits.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@changjoon-park
Round float at the decimal level to match CPython's _Py_dg_dtoa
2310495
CPython's `float.__round__` (Objects/floatobject.c) routes through
`_Py_dg_dtoa` and rounds at the decimal level. The previous
`round_float_digits` multiplied by 10**ndigits and rounded at the
IEEE 754 binary level, which diverges for values that aren't exactly
representable. For example, 2.675 stores as 2.67499...; dtoa correctly
rounds it down to 2.67, but `(2.675 * 100.0).round() / 100.0` lands on
2.68 because the multiplication produces a phantom 267.5 tie that
round-half-to-even snaps up.

Rust's `{:.*}` float formatting uses dtoa-style algorithms (Grisu3 +
Dragon4 fallback) and matches CPython's `_Py_dg_dtoa` byte-for-byte.
Replace the multiply-then-round step with `format!` + `parse` for
ndigits >= 0. The ndigits < 0 path is unchanged because dividing
typical inputs by 10**|ndigits| produces genuine ties rather than
synthesizing them.

Verified byte-identical with CPython 3.14.4 over a 108-case random
fuzz plus targeted half-tie probes. Unmasks
`test_float.RoundTestCase.test_matches_float_format` and
`test_previous_round_bugs`.
@coderabbitai

coderabbitai Bot commented May 2, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 741c2592-e9ac-4826-81f0-ef8d550e2a49

📥 Commits

Reviewing files that changed from the base of the PR and between 2310495 and f039f28.

📒 Files selected for processing (1)
  • crates/common/src/float_ops.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/common/src/float_ops.rs

📝 Walkthrough

Walkthrough

Rewrites round_float_digits() to mirror CPython: early non-finite returns, clamps ndigits, uses format+parse for ndigits >= 0, divide-then-round with explicit 0.5 tie correction for ndigits < 0, and validates finiteness of the result; adds tests exercising banker's rounding and edge cases.

Changes

Float Rounding Implementation

Layer / File(s) Summary
Imports
crates/common/src/float_ops.rs
Removed unused Float and Zero from num_traits imports.
Behavioral/Data Shape
crates/common/src/float_ops.rs
Added early return for non-finite inputs and computed NDIGITS_MAX/NDIGITS_MIN bounds for clamping ndigits.
Core Implementation
crates/common/src/float_ops.rs
Replaced previous pow-splitting/overflow-avoidance and special-case ndigits == 0 logic. For ndigits >= 0 use format!("{:.*}", ndigits, x) then parse() back to f64; for ndigits < 0 use divide-then-round via powi with explicit 0.5 tie handling (banker's rounding). Final result is checked for finiteness and mapped to None if non-finite.
Tests / Examples
extra_tests/snippets/builtin_round.py
Added import math and ~50 assertions covering decimal banker's rounding (including IEEE-754 artifacts), exact-halfway ties, negative ndigits, NaN/infinity behavior with ndigits, signed zero preservation, and out-of-range ndigits short-circuit cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Fix process abort on large float format precision #7633: Adds precision-clamping helpers for runtime precision in format!("{:.*}", ...); relevant because this PR newly uses runtime-format precision for ndigits >= 0.

Suggested reviewers

  • ShaharNaveh
  • youknowone

Poem

🐇 I hop on floats both near and far,

I nudge the digits, round like a star —
Banker's balance, ties resolved clean,
Signed zeros kept, and NaN sits serene.
Hop, hop, let's ship this rounding scene!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: updating float rounding to match CPython's behavior using decimal-level rounding via _Py_dg_dtoa.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
  • X
  • Mastodon
  • Reddit
  • LinkedIn
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions

github-actions Bot commented May 2, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] test: cpython/Lib/test/test_float.py (TODO: 4)
[x] test: cpython/Lib/test/test_strtod.py (TODO: 6)

dependencies:

dependent tests: (no tests depend on float)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

ShaharNaveh
ShaharNaveh requested changes May 2, 2026
View reviewed changes

@ShaharNaveh ShaharNaveh left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall lgtm!
I had a minor nitpick

tysm for the patches:)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/common/src/float_ops.rs Outdated
let pow1 = 10.0f64.powi(-ndigits);
let y = x / pow1;
let z = y.round();
#[allow(clippy::float_cmp)]

@ShaharNaveh ShaharNaveh May 2, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(clippy::float_cmp)]
#[expect(clippy::float_cmp, reason = "...")]

Can you please change it to expect and add a reason to this cfg?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@changjoon-park changjoon-park May 2, 2026

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch i'll keep this convention following prs too thanks !

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@changjoon-park @ShaharNaveh
Use #[expect] with reason for float_cmp suppression
f039f28
Co-authored-by: ShaharNaveh <[email protected]>
ShaharNaveh
ShaharNaveh approved these changes May 2, 2026
View reviewed changes

@ShaharNaveh ShaharNaveh left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tysm:)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@changjoon-park

changjoon-park commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

tysm:)

thanks for the quick review !!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

youknowone
youknowone approved these changes May 2, 2026
View reviewed changes
@youknowone
youknowone merged commit c2910c0 into RustPython:main May 2, 2026
21 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@youknowone youknowone mentioned this pull request Jul 9, 2026
round(number, digits) is incorrect for non-zero digits #1554
Closed
youknowone pushed a commit that referenced this pull request Sep 16, 2026
@changjoon-park @ShaharNaveh
Round float at the decimal level to match CPython's _Py_dg_dtoa (#7761)
2d485de
* Round float at the decimal level to match CPython's _Py_dg_dtoa

CPython's `float.__round__` (Objects/floatobject.c) routes through
`_Py_dg_dtoa` and rounds at the decimal level. The previous
`round_float_digits` multiplied by 10**ndigits and rounded at the
IEEE 754 binary level, which diverges for values that aren't exactly
representable. For example, 2.675 stores as 2.67499...; dtoa correctly
rounds it down to 2.67, but `(2.675 * 100.0).round() / 100.0` lands on
2.68 because the multiplication produces a phantom 267.5 tie that
round-half-to-even snaps up.

Rust's `{:.*}` float formatting uses dtoa-style algorithms (Grisu3 +
Dragon4 fallback) and matches CPython's `_Py_dg_dtoa` byte-for-byte.
Replace the multiply-then-round step with `format!` + `parse` for
ndigits >= 0. The ndigits < 0 path is unchanged because dividing
typical inputs by 10**|ndigits| produces genuine ties rather than
synthesizing them.

Verified byte-identical with CPython 3.14.4 over a 108-case random
fuzz plus targeted half-tie probes. Unmasks
`test_float.RoundTestCase.test_matches_float_format` and
`test_previous_round_bugs`.

* Use #[expect] with reason for float_cmp suppression

Co-authored-by: ShaharNaveh <[email protected]>

---------

Co-authored-by: ShaharNaveh <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@youknowone youknowone youknowone approved these changes
@ShaharNaveh ShaharNaveh ShaharNaveh approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@changjoon-park @youknowone @ShaharNaveh
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.