Skip to content

[ruff] Avoid false positives for overloaded division (RUF069) - #28309

Merged
ntBre merged 1 commit into
astral-sh:mainfrom
IbrahimKhan12:fix-ruf069-overloaded-division
Sep 17, 2026
Merged

ntBre merged 1 commit into
astral-sh:mainfrom
IbrahimKhan12:fix-ruf069-overloaded-division

Conversation

@IbrahimKhan12

@IbrahimKhan12 IbrahimKhan12 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #23281.

RUF069 currently treats division as a floating-point expression whenever either operand is syntactically numeric. This works for built-in numeric division, but Python allows / to be overloaded. As a result, the rule reports false positives for Decimal and custom numeric classes whose division returns an exact, non-float value.

This change removes the division-specific heuristic. Division now follows the same logic as other binary expressions: Ruff reports the comparison only when it can identify a float or complex value in the expression.

value / 2 == expected         # No diagnostic when the type of value is unknown.
value / 2.0 == expected       # RUF069 because 2.0 is a known float.
Decimal("1") / 2 == expected  # No diagnostic for overloaded division.

Without type inference, Ruff cannot determine whether value / 2 uses built-in integer division or an overloaded operator. This change therefore favors false negatives over false positives when the operand types are unknown. Explicit float and complex values remain detected. Future type inference could recover built-in integer-division cases without reintroducing false positives for overloaded operators.

The regression fixture covers direct and reverse Decimal division, marks unknown-type division as accepted, and verifies that a float nested inside division still triggers the rule.

Test Plan

  • INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ruff_linter ruf069
  • INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run (9,203 passed, 44 skipped)
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo dev generate-all
  • Ran prek for all changed files
  • Verified the Decimal reproduction with the Ruff CLI

@IbrahimKhan12
IbrahimKhan12 marked this pull request as ready for review September 4, 2026 10:55
@astral-sh-bot
astral-sh-bot Bot requested a review from ntBre September 4, 2026 10:55
@ntBre ntBre added bug An issue describing something that isn't working, or a PR that fixes a bug rule Implementing or modifying a lint rule preview Related to preview-mode features labels Sep 4, 2026

@ntBre ntBre left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks! This looks good, just a couple of very small nits.

@astral-sh-bot

astral-sh-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+6 -51 violations, +0 -0 fixes in 8 projects; 51 projects unchanged)

PlasmaPy/PlasmaPy (+0 -10 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

- src/plasmapy/formulary/quantum.py:123:10: float-equality-comparison Unreliable floating point equality comparison `0 * u.m / u.s == V`
- tests/formulary/test_fusion.py:326:16: float-equality-comparison Unreliable floating point equality comparison `sigma.unit == u.m**3 / u.s`
- tests/formulary/test_fusion.py:430:16: float-equality-comparison Unreliable floating point equality comparison `sv.unit == u.m**3 / u.s`
- tests/formulary/test_fusion.py:451:16: float-equality-comparison Unreliable floating point equality comparison `sv.unit == u.m**3 / u.s`
- tests/formulary/test_laser.py:89:12: float-equality-comparison Unreliable floating point equality comparison `result.unit == u.Watt / u.m**2`
- tests/formulary/test_misc.py:131:12: float-equality-comparison Unreliable floating point equality comparison `(Bohm_diffusion(T_e, B)).unit == u.m**2 / u.s`
- tests/plasma/sources/test_plasma3d.py:48:12: float-equality-comparison Unreliable floating point equality comparison `test_plasma.density.si.unit == u.kg / u.m**3`
- tests/plasma/sources/test_plasma3d.py:51:12: float-equality-comparison Unreliable floating point equality comparison `test_plasma.momentum.si.unit == u.kg / (u.m**2 * u.s)`
- tests/plasma/sources/test_plasmablob.py:51:12: float-equality-comparison Unreliable floating point equality comparison `test_plasma.density.si.unit == u.kg / u.m**3`
- tests/plasma/sources/test_plasmablob.py:54:12: float-equality-comparison Unreliable floating point equality comparison `test_plasma.momentum.si.unit == u.kg / (u.m**2 * u.s)`

lnbits/lnbits (+0 -1 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

- tests/unit/test_services_fees.py:61:12: float-equality-comparison Unreliable floating point equality comparison `fee / 1000 == 199`

milvus-io/pymilvus (+0 -1 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

- examples/orm_deprecated/bulk_import/example_bulkwriter_with_nullable.py:300:16: float-equality-comparison Unreliable floating point equality comparison `len(b[0]) == DIM/8`

pandas-dev/pandas (+0 -12 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

- pandas/core/indexes/range.py:1022:30: float-equality-comparison Unreliable floating point equality comparison `abs(start_s - start_o) == step_s / 2`
- pandas/core/indexes/range.py:1023:30: float-equality-comparison Unreliable floating point equality comparison `abs(end_s - end_o) == step_s / 2`
- pandas/core/indexes/range.py:1237:41: float-equality-comparison Unreliable floating point equality comparison `item == self[0] + self.step / 2`
- pandas/tests/scalar/timedelta/test_arithmetic.py:799:16: float-equality-comparison Unreliable floating point equality comparison `result == td._value / (86400 * 10**9)`
- pandas/tests/scalar/timedelta/test_arithmetic.py:801:16: float-equality-comparison Unreliable floating point equality comparison `result == td._value / 10**9`
- pandas/tests/scalar/timedelta/test_timedelta.py:139:16: float-equality-comparison Unreliable floating point equality comparison `res._value == td._value / 2`
- pandas/tests/scalar/timedelta/test_timedelta.py:143:16: float-equality-comparison Unreliable floating point equality comparison `res._value == td._value / 2`
- pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py:70:13: float-equality-comparison Unreliable floating point equality comparison `pd.Timestamp(pydt_max).as_unit("ns")._value / 1000 == pd.Timestamp.max._value / 1000`
- pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py:84:13: float-equality-comparison Unreliable floating point equality comparison `pd.Timestamp(pydt_min + tdus).as_unit("ns")._value / 1000 == pd.Timestamp.min._value / 1000`
- pandas/tests/tseries/offsets/test_ticks.py:255:12: float-equality-comparison Unreliable floating point equality comparison `off / 2 == cls(5)`
... 2 additional changes omitted for project

prefecthq/prefect (+0 -1 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

- tests/events/server/test_events_counts.py:525:12: float-equality-comparison Unreliable floating point equality comparison `len(counts) == (until - since).total_seconds() / 1800 + 1`

indico/indico (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ indico/modules/designer/util.py:108:22: RUF069 Unreliable floating point equality comparison `frm_size[0] == float(tpl.data['width']) / PIXELS_CM`
+ indico/modules/designer/util.py:109:22: RUF069 Unreliable floating point equality comparison `frm_size[1] == float(tpl.data['height']) / PIXELS_CM`

astropy/astropy (+0 -25 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

- astropy/coordinates/tests/test_frames_with_velocity.py:218:12: float-equality-comparison Unreliable floating point equality comparison `icrs.pm_ra == 10 * u.mas / u.yr`
- astropy/coordinates/tests/test_frames_with_velocity.py:226:12: float-equality-comparison Unreliable floating point equality comparison `icrs.pm_ra == 10 * u.mas / u.yr`
- astropy/coordinates/tests/test_frames_with_velocity.py:246:12: float-equality-comparison Unreliable floating point equality comparison `icrs.v_x == 1 * u.km / u.s`
- astropy/coordinates/tests/test_frames_with_velocity.py:247:12: float-equality-comparison Unreliable floating point equality comparison `icrs.v_y == 2 * u.km / u.s`
- astropy/coordinates/tests/test_frames_with_velocity.py:248:12: float-equality-comparison Unreliable floating point equality comparison `icrs.v_z == 3 * u.km / u.s`
- astropy/coordinates/tests/test_representation.py:126:16: float-equality-comparison Unreliable floating point equality comparison `newrep.differentials["s"].d_distance == -9 * u.km / u.s`
- astropy/cosmology/_src/tests/flrw/test_base.py:192:16: float-equality-comparison Unreliable floating point equality comparison `cosmo.critical_density0.unit == u.g / u.cm**3`
- astropy/cosmology/_src/tests/flrw/test_lambdacdm.py:633:12: float-equality-comparison Unreliable floating point equality comparison `cosmo.critical_density(1.0).unit == u.g / u.cm**3`
- astropy/cosmology/_src/tests/test_units.py:57:13: float-equality-comparison Unreliable floating point equality comparison `3 * u.km / cu.redshift**3 == 3 * u.km`
- astropy/io/fits/util.py:245:12: float-equality-comparison Unreliable floating point equality comparison `ns.dtype.itemsize != s.dtype.itemsize / 4`
- astropy/io/fits/util.py:278:12: float-equality-comparison Unreliable floating point equality comparison `ns.dtype.itemsize / 4 != s.dtype.itemsize`
- astropy/modeling/tests/test_physical_models.py:69:12: float-equality-comparison Unreliable floating point equality comparison `b(1.0 * u.micron).unit == u.erg / (u.cm**2 * u.s * u.Hz * u.sr)`
- astropy/nddata/ccddata.py:77:16: float-equality-comparison Unreliable floating point equality comparison `unit == 1 / (parent_unit**2)`
- astropy/time/tests/test_methods.py:771:20: float-equality-comparison Unreliable floating point equality comparison `t.mean() == t1 + epsilon / 2`
- astropy/units/tests/test_format.py:967:12: float-equality-comparison Unreliable floating point equality comparison `x == number * (u.erg / u.s / u.cm**2 / u.Angstrom)`
- astropy/units/tests/test_format.py:971:12: float-equality-comparison Unreliable floating point equality comparison `x == number * (u.erg / u.s / u.cm**2 / u.Angstrom)`
- astropy/units/tests/test_quantity.py:608:17: float-equality-comparison Unreliable floating point equality comparison `1 / (u.cm * u.cm) == 1 * u.cm**-2`
- astropy/units/tests/test_quantity.py:855:12: float-equality-comparison Unreliable floating point equality comparison `q.si.unit == 1 / u.m`
- astropy/units/tests/test_quantity.py:869:12: float-equality-comparison Unreliable floating point equality comparison `q.cgs.unit == 1 / u.cm`
- astropy/units/tests/test_quantity_array_methods.py:434:16: float-equality-comparison Unreliable floating point equality comparison `q1[1] == 1000 * u.m / u.km`
- astropy/units/tests/test_quantity_array_methods.py:436:16: float-equality-comparison Unreliable floating point equality comparison `q1[1] == 1 * u.m / u.km`
... 4 additional changes omitted for project

home-assistant/core (+4 -1 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ tests/components/vizio/test_media_player.py:135:9: float-equality-comparison Unreliable floating point equality comparison `attr["volume_level"] == float(int(MAX_VOLUME[vizio_device_class] / 2))
+ tests/components/vizio/test_media_player.py:258:17: float-equality-comparison Unreliable floating point equality comparison `attr["volume_level"] == float(int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2))
+ tests/components/vizio/test_media_player.py:353:17: float-equality-comparison Unreliable floating point equality comparison `attr["volume_level"] == float(volume) / MAX_VOLUME[MediaPlayerDeviceClass.TV]`
+ tests/components/vizio/test_media_player.py:384:17: float-equality-comparison Unreliable floating point equality comparison `attr["volume_level"] == float(volume) / MAX_VOLUME[MediaPlayerDeviceClass.TV]`
- tests/util/test_unit_conversion.py:1358:12: float-equality-comparison Unreliable floating point equality comparison `converter.get_unit_floored_log_ratio(to_unit, from_unit) == 1 / ratio`

Changes by rule (2 rules affected)

code total + violation - violation + fix - fix
float-equality-comparison 55 4 51 0 0
RUF069 2 2 0 0 0

- Treat division like other binary expressions when detecting floats.
- Cover built-in and Decimal division regression cases.
@IbrahimKhan12
IbrahimKhan12 force-pushed the fix-ruf069-overloaded-division branch from 21feb44 to 0612898 Compare September 6, 2026 19:16
@IbrahimKhan12

Copy link
Copy Markdown
Contributor Author

nits addressed!

@IbrahimKhan12
IbrahimKhan12 requested a review from ntBre September 6, 2026 19:23

@ntBre ntBre left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you!

@ntBre
ntBre enabled auto-merge (squash) September 17, 2026 21:21
@ntBre
ntBre merged commit eb3751b into astral-sh:main Sep 17, 2026
48 of 49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug An issue describing something that isn't working, or a PR that fixes a bug preview Related to preview-mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RUF069 mistaken report with custom Decimal class

2 participants