Skip to content

[ruff] New rule float-comparison (RUF069)#20585

Merged
ntBre merged 35 commits intoastral-sh:mainfrom
chirizxc:sq-S1244
Feb 5, 2026
Merged

[ruff] New rule float-comparison (RUF069)#20585
ntBre merged 35 commits intoastral-sh:mainfrom
chirizxc:sq-S1244

Conversation

@chirizxc
Copy link
Contributor

@chirizxc chirizxc commented Sep 25, 2025

Summary

Part of #14220

Test Plan

cargo nextest run ruf069

@chirizxc
Copy link
Contributor Author

We need more tests and cases where the rule can give FP and so on

@chirizxc
Copy link
Contributor Author

Also, I randomly picked CODE for this rule

@ntBre
Copy link
Contributor

ntBre commented Sep 25, 2025

I haven't reviewed, but if there's not a corresponding E723 rule in pycodestyle, I would default to making this a ruff rule. We have a PR in progress for RUF066, so RUF067 would be the next free code.

@ntBre ntBre added rule Implementing or modifying a lint rule preview Related to preview mode features labels Sep 25, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Sep 25, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+1130 -0 violations, +0 -0 fixes in 15 projects; 40 projects unchanged)

RasaHQ/rasa (+35 -0 violations, +0 -0 fixes)

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

+ .github/tests/test_mr_publish_results.py:63:12: RUF069 Unreliable floating point equality comparison `87.0 == transform_to_seconds("1m27s")`
+ .github/tests/test_mr_publish_results.py:64:12: RUF069 Unreliable floating point equality comparison `87.3 == transform_to_seconds("1m27.3s")`
+ .github/tests/test_mr_publish_results.py:65:12: RUF069 Unreliable floating point equality comparison `27.0 == transform_to_seconds("27s")`
+ .github/tests/test_mr_publish_results.py:66:12: RUF069 Unreliable floating point equality comparison `3627.0 == transform_to_seconds("1h27s")`
+ .github/tests/test_mr_publish_results.py:67:12: RUF069 Unreliable floating point equality comparison `3687.0 == transform_to_seconds("1h1m27s")`
+ rasa/core/policies/ensemble.py:57:32: RUF069 Unreliable floating point equality comparison `max_confidence == 0.0`
+ rasa/engine/caching.py:301:16: RUF069 Unreliable floating point equality comparison `self._max_cache_size == 0.0`
+ rasa/shared/core/slots.py:257:16: RUF069 Unreliable floating point equality comparison `x == 1.0`
+ rasa/shared/core/slots.py:260:20: RUF069 Unreliable floating point equality comparison `float(x) == 1.0`
+ rasa/utils/tensorflow/layers.py:291:12: RUF069 Unreliable floating point equality comparison `self.density == 1.0`
... 25 additional changes omitted for project

apache/airflow (+31 -0 violations, +0 -0 fixes)

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

+ airflow-core/src/airflow/models/taskinstance.py:958:12: RUF069 Unreliable floating point equality comparison `multiplier != 1.0`
+ airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_task_instances.py:1369:16: RUF069 Unreliable floating point equality comparison `ti.duration == 3600.00`
+ airflow-core/tests/unit/cli/commands/test_variable_command.py:205:16: RUF069 Unreliable floating point equality comparison `Variable.get("float", deserialize_json=True) == 42.0`
+ airflow-core/tests/unit/core/test_configuration.py:376:16: RUF069 Unreliable floating point equality comparison `test_conf.getfloat("another", "key8_float", fallback="1") == 1.0`
+ airflow-core/tests/unit/jobs/test_base_job.py:113:20: RUF069 Unreliable floating point equality comparison `most_recent.heartrate == float(job_heartbeat_sec)`
+ airflow-core/tests/unit/jobs/test_base_job.py:298:16: RUF069 Unreliable floating point equality comparison `health_check_threshold("UnknownJob", 30) == 30 * 2.1`
+ airflow-core/tests/unit/jobs/test_scheduler_job.py:7067:24: RUF069 Unreliable floating point equality comparison `duration == 0.0`
+ airflow-core/tests/unit/jobs/test_scheduler_job.py:7093:24: RUF069 Unreliable floating point equality comparison `duration == 0.0`
+ airflow-core/tests/unit/serialization/test_dag_serialization.py:4193:16: RUF069 Unreliable floating point equality comparison `result["retry_delay"] == 300.0`
+ airflow-core/tests/unit/utils/test_sqlalchemy.py:119:16: RUF069 Unreliable floating point equality comparison `logical_date.utcoffset().total_seconds() == 0.0`
... 21 additional changes omitted for project

apache/superset (+31 -0 violations, +0 -0 fixes)

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

+ tests/integration_tests/utils_tests.py:406:16: RUF069 Unreliable floating point equality comparison `cast_to_num("5.2") == 5.2`
+ tests/integration_tests/utils_tests.py:408:16: RUF069 Unreliable floating point equality comparison `cast_to_num(10.1) == 10.1`
+ tests/unit_tests/commands/databases/csv_reader_test.py:853:12: RUF069 Unreliable floating point equality comparison `df.iloc[0]["Score"] == 95.5`
+ tests/unit_tests/commands/databases/csv_reader_test.py:950:12: RUF069 Unreliable floating point equality comparison `result_df.iloc[0]["score"] == 95.5`
+ tests/unit_tests/commands/report/alert_test.py:226:12: RUF069 Unreliable floating point equality comparison `command._result == 0.0`
+ tests/unit_tests/commands/report/alert_test.py:229:12: RUF069 Unreliable floating point equality comparison `report_schedule_mock.last_value == 0.0`
+ tests/unit_tests/dataframe_test.py:264:12: RUF069 Unreliable floating point equality comparison `records[3]["result"] == 0.0`
+ tests/unit_tests/dataframe_test.py:265:12: RUF069 Unreliable floating point equality comparison `records[4]["result"] == 42.5`
+ tests/unit_tests/dataframe_test.py:308:12: RUF069 Unreliable floating point equality comparison `records_without_fix[1]["test"] == 5.0`
+ tests/unit_tests/dataframe_test.py:358:12: RUF069 Unreliable floating point equality comparison `parsed[0]["value1"] == 100.0`
... 21 additional changes omitted for project

bokeh/bokeh (+197 -0 violations, +0 -0 fixes)

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

+ examples/topics/categorical/slope_graph.py:29:10: RUF069 Unreliable floating point equality comparison `df["year"] == 2000.0`
+ examples/topics/categorical/slope_graph.py:29:35: RUF069 Unreliable floating point equality comparison `df["year"] == 2010.0`
+ src/bokeh/colors/color.py:321:12: RUF069 Unreliable floating point equality comparison `self.a == 1.0`
+ src/bokeh/colors/color.py:460:12: RUF069 Unreliable floating point equality comparison `self.a == 1.0`
+ tests/integration/tools/test_range_tool.py:103:16: RUF069 Unreliable floating point equality comparison `results['start'] == 0.4`
+ tests/integration/tools/test_range_tool.py:104:16: RUF069 Unreliable floating point equality comparison `results['end'] == 0.6`
+ tests/integration/tools/test_range_tool.py:118:16: RUF069 Unreliable floating point equality comparison `results['start'] == 0.5`
+ tests/integration/tools/test_range_tool.py:119:16: RUF069 Unreliable floating point equality comparison `results['end'] == 0.7`
+ tests/integration/tools/test_range_tool.py:126:16: RUF069 Unreliable floating point equality comparison `results['start'] == 0.2`
+ tests/integration/tools/test_range_tool.py:127:16: RUF069 Unreliable floating point equality comparison `results['end'] == 0.4`
... 187 additional changes omitted for project

ibis-project/ibis (+29 -0 violations, +0 -0 fixes)

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

+ ibis/backends/bigquery/tests/system/test_client.py:438:48: RUF069 Unreliable floating point equality comparison `t.max != 9999.9`
+ ibis/backends/bigquery/tests/system/udf/test_udf_execute.py:102:12: RUF069 Unreliable floating point equality comparison `con.execute(expr) == 8.0`
+ ibis/backends/clickhouse/tests/test_operators.py:175:12: RUF069 Unreliable floating point equality comparison `round(con.execute(expr), 3) == -5.245`
+ ibis/backends/druid/tests/conftest.py:58:31: RUF069 Unreliable floating point equality comparison `js[datasource] == 100.0`
+ ibis/backends/duckdb/tests/test_udf.py:180:12: RUF069 Unreliable floating point equality comparison `result.iat[0] == 1.0`
+ ibis/backends/materialize/tests/test_client.py:318:16: RUF069 Unreliable floating point equality comparison `value[0].as_py() == 1.0`
+ ibis/backends/postgres/tests/test_client.py:515:12: RUF069 Unreliable floating point equality comparison `value[0].as_py() == 1.0`
+ ibis/backends/sqlite/tests/test_client.py:56:12: RUF069 Unreliable floating point equality comparison `result == 0.0`
+ ibis/backends/tests/test_map.py:504:12: RUF069 Unreliable floating point equality comparison `con.execute(expr) == 3.0`
+ ibis/backends/tests/test_numeric.py:1217:12: RUF069 Unreliable floating point equality comparison `result == 0.5`
... 19 additional changes omitted for project

langchain-ai/langchain (+71 -0 violations, +0 -0 fixes)

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

+ libs/core/tests/unit_tests/language_models/chat_models/test_base.py:1207:12: RUF069 Unreliable floating point equality comparison `ls_params["ls_temperature"] == 0.2`
+ libs/core/tests/unit_tests/language_models/llms/test_base.py:273:12: RUF069 Unreliable floating point equality comparison `ls_params["ls_temperature"] == 0.2`
+ libs/core/tests/unit_tests/output_parsers/test_openai_tools.py:1150:12: RUF069 Unreliable floating point equality comparison `result_v2[0].coordinates.latitude == 48.8584`
+ libs/core/tests/unit_tests/output_parsers/test_openai_tools.py:1151:12: RUF069 Unreliable floating point equality comparison `result_v2[0].coordinates.longitude == 2.2945`
+ libs/core/tests/unit_tests/output_parsers/test_openai_tools.py:1190:12: RUF069 Unreliable floating point equality comparison `result_mixed[1].coordinates.latitude == 37.8199`
+ libs/core/tests/unit_tests/output_parsers/test_openai_tools.py:1246:12: RUF069 Unreliable floating point equality comparison `result_v1_full[0].price == 999.99`
+ libs/core/tests/unit_tests/output_parsers/test_openai_tools.py:1268:12: RUF069 Unreliable floating point equality comparison `result_v1_minimal[0].price == 29.99`
+ libs/core/tests/unit_tests/rate_limiters/test_in_memory_rate_limiter.py:104:16: RUF069 Unreliable floating point equality comparison `rate_limiter.available_tokens == 199.0`
+ libs/core/tests/unit_tests/rate_limiters/test_in_memory_rate_limiter.py:107:16: RUF069 Unreliable floating point equality comparison `rate_limiter.available_tokens == 499.0`
+ libs/core/tests/unit_tests/rate_limiters/test_in_memory_rate_limiter.py:21:12: RUF069 Unreliable floating point equality comparison `rate_limiter.available_tokens == 0.0`
... 61 additional changes omitted for project

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

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

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

milvus-io/pymilvus (+17 -0 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: RUF069 Unreliable floating point equality comparison `len(b[0]) == DIM/8`
+ pymilvus/orm/iterator.py:557:12: RUF069 Unreliable floating point equality comparison `self._width == 0.0`
+ tests/orm/test_utility.py:346:16: RUF069 Unreliable floating point equality comparison `result == 0.0`
+ tests/orm/test_utility.py:355:16: RUF069 Unreliable floating point equality comparison `result == 1000.0`
+ tests/test_client_types.py:831:16: RUF069 Unreliable floating point equality comparison `extra["cache_hit_ratio"] == 0.75`
+ tests/test_search_result.py:1195:16: RUF069 Unreliable floating point equality comparison `val == 1.0`
+ tests/test_search_result.py:1203:16: RUF069 Unreliable floating point equality comparison `extract_struct_field_value(fd, 0) == 1.0`
+ tests/test_search_result.py:1348:16: RUF069 Unreliable floating point equality comparison `info.last_bound == 0.0`
+ tests/test_search_result.py:154:16: RUF069 Unreliable floating point equality comparison `first_hit["distance"] == 0.0`
+ tests/test_search_result.py:681:16: RUF069 Unreliable floating point equality comparison `sr.extra["cache_hit_ratio"] == 0.5`
... 7 additional changes omitted for project

... Truncated remaining completed project reports due to GitHub comment length restrictions

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF069 1130 1130 0 0 0

@chirizxc chirizxc changed the title [pycodestyle] New rule float-comparison (E723) [ruff] New rule float-comparison (RUF067) Sep 25, 2025
@chirizxc
Copy link
Contributor Author

chirizxc commented Sep 25, 2025

изображение

This breaks the behavior of code and results in:

TypeError: cannot convert the series to <class 'float'>

@chirizxc
Copy link
Contributor Author

What should we do in such a case? Should we somehow mark this fact in the documentation or what should we do, because we will not be able to check in general that such calls will not lead to some code breakages

@chirizxc
Copy link
Contributor Author

The rule detects potentially problematic float comparisons correctly, but we should not suggest replacing only math.isclose() but also numpy.isclose()

@ntBre
Copy link
Contributor

ntBre commented Oct 27, 2025

Would it help with the false positives, and maybe with the numpy cases, to apply the rule only in boolean contexts? I think we have other rules that apply only to expressions used as if conditions or in negated not expressions, for example. We even have a semantic model helper for it:

pub const fn in_boolean_test(&self) -> bool {

This rule is going to require type inference to get exactly right anyway, so this might be a good way to be more conservative in the meantime.

I also only gave the implementation a quick skim and could have overlooked this, but does this rule apply to comparisons like <= and > too? I would probably expect those to be allowed since something like delta < 1e-8 should work as expected, I'm assuming that's what math.isclose does under the hood. Along those lines, we may want to rename the rule to float-equality-comparison, if it only applies to equality.

@chirizxc
Copy link
Contributor Author

Would it help with the false positives, and maybe with the numpy cases, to apply the rule only in boolean contexts? I think we have other rules that apply only to expressions used as if conditions or in negated not expressions, for example. We even have a semantic model helper for it:

pub const fn in_boolean_test(&self) -> bool {

This rule is going to require type inference to get exactly right anyway, so this might be a good way to be more conservative in the meantime.

I also only gave the implementation a quick skim and could have overlooked this, but does this rule apply to comparisons like <= and > too? I would probably expect those to be allowed since something like delta < 1e-8 should work as expected, I'm assuming that's what math.isclose does under the hood. Along those lines, we may want to rename the rule to float-equality-comparison, if it only applies to equality.

Yes, this only applies to == and !=, just as it works in SonarQube.

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you! And sorry for the delay on the review.

I think this will be a really helpful rule, but I'm a bit wary of recommending np.isclose. What do you think about trying to restrict the rule to plain floats for now? We could always expand the scope later.

My other comments were just small suggestions.

@chirizxc
Copy link
Contributor Author

chirizxc commented Jan 1, 2026

Thank you! And sorry for the delay on the review.

I think this will be a really helpful rule, but I'm a bit wary of recommending np.isclose. What do you think about trying to restrict the rule to plain floats for now? We could always expand the scope later.

My other comments were just small suggestions.

Then we will severely restrict the rule, since comparing two floats is less common

@chirizxc
Copy link
Contributor Author

chirizxc commented Jan 1, 2026

Essentially, math.isclose() / numpy.isclose() should cover 100% of cases.

@chirizxc chirizxc changed the title [ruff] New rule float-comparison (RUF067) [ruff] New rule float-comparison (RUF070) Jan 1, 2026
@chirizxc
Copy link
Contributor Author

chirizxc commented Jan 24, 2026

I also slightly updated the code to find the following cases:

def foo(a, b):
    return a == b - 0.1

def foo(a, b):
    return a == b - float(“2”)

Ecosystem report should become larger (upd. 1008 -> 1148)

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks! I found a couple more very small nits in the code, and I also found some interesting cases in the ecosystem report:

I think these are technically false positives because pytest.approx is another way to solve the approximate equality issue.

If I understand correctly, this is actually a safe comparison because two infinities can be equal, unlike NaNs.

I think we should try to suppress these two cases before landing.

chirizxc and others added 6 commits January 26, 2026 22:34
…son.rs

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
…son.rs

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
@chirizxc
Copy link
Contributor Author

Thanks! I found a couple more very small nits in the code, and I also found some interesting cases in the ecosystem report:

* [tests/integration/tools/test_box_zoom_tool.py:106:16:](https://github.com/bokeh/bokeh/blob/829b2a75c402d0d0abd7e37ff201fbdfd949d857/tests/integration/tools/test_box_zoom_tool.py#L106) RUF069 Unreliable floating point equality comparison `(results['xrstart'] + results['xrend'])/2.0 == pytest.approx(0.25)`

* [tests/integration/tools/test_box_zoom_tool.py:107:16:](https://github.com/bokeh/bokeh/blob/829b2a75c402d0d0abd7e37ff201fbdfd949d857/tests/integration/tools/test_box_zoom_tool.py#L107) RUF069 Unreliable floating point equality comparison `(results['yrstart'] + results['yrend'])/2.0 == pytest.approx(0.75)`

I think these are technically false positives because pytest.approx is another way to solve the approximate equality issue.

* [airflow-core/tests/unit/models/test_pool.py:205:16:](https://github.com/apache/airflow/blob/0cf89fa362e72898b718fe6ff2fc8caa0b4ba45f/airflow-core/tests/unit/models/test_pool.py#L205) RUF069 Unreliable floating point equality comparison `float("inf") == pool.open_slots()`

If I understand correctly, this is actually a safe comparison because two infinities can be equal, unlike NaNs.

I think we should try to suppress these two cases before landing.

It seems that everything should be fine now.

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks for your patience and all of your work here. I think I found one more issue with math.inf, but this otherwise looks great to me.

@chirizxc chirizxc requested a review from ntBre February 4, 2026 22:50
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you! I pushed two commits slightly simplifying the math.inf checks (resolve_qualified_name already handles both names and attributes) and adding a test case for complex("inf"). I think this is ready to land!

@ntBre ntBre merged commit d70f33c into astral-sh:main Feb 5, 2026
42 checks passed
@chirizxc chirizxc deleted the sq-S1244 branch February 5, 2026 19:57
ntBre pushed a commit that referenced this pull request Feb 6, 2026
## Summary

See: #20585

## Test Plan

`cargo nextest run ruf069`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

6 participants