Skip to content

[ruff] Suppress diagnostic for invalid f-strings before Python 3.12 (RUF027)#23480

Merged
ntBre merged 8 commits intoastral-sh:mainfrom
stakeswky:fix/ruf027-comment-py311
Feb 25, 2026
Merged

[ruff] Suppress diagnostic for invalid f-strings before Python 3.12 (RUF027)#23480
ntBre merged 8 commits intoastral-sh:mainfrom
stakeswky:fix/ruf027-comment-py311

Conversation

@stakeswky
Copy link
Contributor

Summary

Fixes #23460.

Comments inside f-string interpolations (#) are only valid in Python 3.12+ (PEP 701). RUF027 was not checking for this, so it could produce invalid f-strings when the interpolation text contained a # and target-version was below 3.12.

Fix

Extended the existing target_version < PY312 backslash check to also cover comments:

if target_version < PythonVersion::PY312
    && (interpolation_text.contains('\\') || interpolation_text.contains('#'))
{
    return false;
}

Test Plan

Added a test fixture with a comment inside an f-string interpolation ({x # }). The existing assert_diagnostics_diff! test for PY311 vs PY312 will capture the new suppression.

Reproducer (from #23460)

x = "!"
print("""{x  # }
}""")

On --target-version py311, this should not trigger RUF027 since the resulting f-string would contain a comment in the interpolation, which is a syntax error before Python 3.12.

…hon < 3.12

Comments inside f-string interpolations are only valid in Python 3.12+.
The RUF027 fix was not checking for this, causing it to produce invalid
f-strings when the interpolation contained a `#` character and the
target version was below 3.12.

This extends the existing backslash check to also cover comments.

Fixes astral-sh#23460
if target_version < PythonVersion::PY312 && interpolation_text.contains('\\') {
if target_version < PythonVersion::PY312
&& (interpolation_text.contains('\\') || interpolation_text.contains('#'))
{

Choose a reason for hiding this comment

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

Number signs are only problematic when they would start comments. f"{'#'}" is fine in Python 3.11.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Updated the check to use a has_comment_hash() helper that tracks string nesting — # inside a nested string literal (e.g., f"{'#'}") is now correctly recognized as not a comment.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 23, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

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

DisnakeDev/disnake (+0 -230 violations, +0 -0 fixes)

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

- disnake/abc.py:1117:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/abc.py:1362:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/abc.py:1402:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/abc.py:1676:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/abc.py:1872:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/abc.py:1915:9: D420 Section "Parameters" appears after section "Examples" but should be before it
- disnake/abc.py:1926:9: D420 Section "Raises" appears after section "Examples" but should be before it
- disnake/abc.py:1931:9: D420 Section "Yields" appears after section "Examples" but should be before it
- disnake/abc.py:1969:9: D420 Section "Parameters" appears after section "Examples" but should be before it
- disnake/abc.py:1993:9: D420 Section "Raises" appears after section "Examples" but should be before it
- disnake/abc.py:2000:9: D420 Section "Yields" appears after section "Examples" but should be before it
- disnake/abc.py:2073:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/abc.py:717:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/abc.py:976:9: D420 Section "Parameters" appears after section "Examples" but should be before it
- disnake/abc.py:989:9: D420 Section "Raises" appears after section "Examples" but should be before it
- disnake/appinfo.py:549:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/asset.py:149:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/asset.py:428:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/asset.py:480:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/asset.py:508:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/asset.py:546:9: D420 Section "Returns" appears after section "Raises" but should be before it
- disnake/asset.py:56:9: D420 Section "Returns" appears after section "Raises" but should be before it
... 208 additional changes omitted for project

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

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

- .github/scripts/mr_publish_results.py:99:5: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/core/actions/action.py:177:5: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/graph_components/validators/finetuning_validator.py:95:9: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/model_testing.py:403:5: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/model_training.py:113:5: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/shared/nlu/training_data/entities_parser.py:167:5: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/shared/utils/io.py:595:5: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/shared/utils/io.py:611:5: D420 Section "Returns" appears after section "Raises" but should be before it
- rasa/utils/tensorflow/model_data.py:976:9: D420 Section "Returns" appears after section "Raises" but should be before it

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

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

- src/plasmapy/formulary/collisions/coulomb.py:439:5: D420 Section "See Also" appears after section "Examples" but should be before it
- src/plasmapy/formulary/collisions/frequencies.py:1081:5: D420 Section "See Also" appears after section "Examples" but should be before it
- src/plasmapy/formulary/collisions/frequencies.py:143:5: D420 Section "See Also" appears after section "Examples" but should be before it
- src/plasmapy/formulary/collisions/frequencies.py:732:5: D420 Section "See Also" appears after section "Examples" but should be before it
- src/plasmapy/formulary/collisions/frequencies.py:917:5: D420 Section "See Also" appears after section "Examples" but should be before it
- src/plasmapy/particles/atomic.py:534:5: D420 Section "See Also" appears after section "Notes" but should be before it
- src/plasmapy/particles/atomic.py:635:5: D420 Section "See Also" appears after section "Notes" but should be before it
- src/plasmapy/particles/atomic.py:755:5: D420 Section "See Also" appears after section "Notes" but should be before it
- src/plasmapy/particles/decorators.py:430:9: D420 Section "See Also" appears after section "Notes" but should be before it
- src/plasmapy/particles/ionization_state_collection.py:117:5: D420 Section "Notes" appears after section "Examples" but should be before it
... 12 additional changes omitted for project

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

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

- providers/standard/src/airflow/providers/standard/operators/hitl.py:264:9: D420 Section "Returns" appears after section "Raises" but should be before it
- task-sdk/src/airflow/sdk/io/path.py:255:9: D420 Section "See Also" appears after section "Examples" but should be before it

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

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

- ibis/backends/__init__.py:1045:9: D420 Section "Returns" appears after section "Notes" but should be before it
- ibis/backends/duckdb/__init__.py:1160:9: D420 Section "See Also" appears after section "Notes" but should be before it
- ibis/backends/materialize/__init__.py:173:9: D420 Section "See Also" appears after section "Examples" but should be before it
- ibis/backends/materialize/__init__.py:208:9: D420 Section "See Also" appears after section "Examples" but should be before it
- ibis/backends/materialize/__init__.py:2234:9: D420 Section "Notes" appears after section "Examples" but should be before it
- ibis/backends/materialize/api.py:144:5: D420 Section "Notes" appears after section "Examples" but should be before it
- ibis/backends/materialize/api.py:150:5: D420 Section "References" appears after section "Examples" but should be before it
- ibis/backends/materialize/api.py:49:5: D420 Section "See Also" appears after section "Examples" but should be before it
- ibis/backends/materialize/api.py:53:5: D420 Section "Notes" appears after section "Examples" but should be before it
- ibis/backends/materialize/api.py:67:5: D420 Section "References" appears after section "Examples" but should be before it
... 47 additional changes omitted for project

langchain-ai/langchain (+2 -42 violations, +0 -0 fixes)

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

- libs/core/langchain_core/_import_utils.py:25:5: D420 Section "Returns" appears after section "Raises" but should be before it
- libs/core/langchain_core/callbacks/manager.py:2323:5: D420 Section "Returns" appears after section "Raises" but should be before it
- libs/core/langchain_core/document_loaders/base.py:71:9: D420 Section "Returns" appears after section "Raises" but should be before it
- libs/core/langchain_core/documents/base.py:164:9: D420 Section "Returns" appears after section "Raises" but should be before it
- libs/core/langchain_core/documents/base.py:182:9: D420 Section "Returns" appears after section "Raises" but should be before it
- libs/core/langchain_core/documents/base.py:201:9: D420 Section "Yields" appears after section "Raises" but should be before it
... 35 additional changes omitted for rule D420
+ libs/core/langchain_core/language_models/llms.py:132:5: DOC501 Raised exception `ValueError` missing from docstring
- libs/core/langchain_core/language_models/llms.py:132:5: DOC501 Raised exception `ValueError` missing from docstring
+ libs/core/langchain_core/messages/utils.py:69:5: DOC501 Raised exception `TypeError` missing from docstring
- libs/core/langchain_core/messages/utils.py:69:5: DOC501 Raised exception `TypeError` missing from docstring
... 34 additional changes omitted for project

reflex-dev/reflex (+0 -35 violations, +0 -0 fixes)

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

- reflex/app.py:1764:5: D420 Section "Yields" appears after section "Raises" but should be before it
- reflex/app.py:2155:9: D420 Section "Args" appears after section "Raises" but should be before it
- reflex/app.py:615:9: D420 Section "Returns" appears after section "Raises" but should be before it
- reflex/app.py:929:9: D420 Section "Args" appears after section "Raises" but should be before it
- reflex/assets.py:50:5: D420 Section "Returns" appears after section "Raises" but should be before it
- reflex/compiler/utils.py:111:5: D420 Section "Returns" appears after section "Raises" but should be before it
- reflex/compiler/utils.py:45:5: D420 Section "Returns" appears after section "Raises" but should be before it
- reflex/components/base/meta.py:20:9: D420 Section "Returns" appears after section "Raises" but should be before it
- reflex/components/component.py:1172:9: D420 Section "Returns" appears after section "Raises" but should be before it
- reflex/components/core/breakpoints.py:69:9: D420 Section "Returns" appears after section "Raises" but should be before it
... 25 additional changes omitted for project

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

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

- astropy/coordinates/attributes.py:51:5: D420 Section "Parameters" appears after section "Examples" but should be before it
- astropy/coordinates/earth.py:385:9: D420 Section "See Also" appears after section "Examples" but should be before it
- astropy/coordinates/sky_coordinate.py:1029:9: D420 Section "See Also" appears after section "Notes" but should be before it
- astropy/coordinates/sky_coordinate.py:1080:9: D420 Section "See Also" appears after section "Notes" but should be before it
- astropy/coordinates/sky_coordinate.py:1121:9: D420 Section "See Also" appears after section "Notes" but should be before it
- astropy/coordinates/sky_coordinate.py:1175:9: D420 Section "See Also" appears after section "Notes" but should be before it
- astropy/coordinates/sky_coordinate.py:1233:9: D420 Section "See Also" appears after section "Notes" but should be before it
- astropy/coordinates/sky_coordinate.py:123:5: D420 Section "Parameters" appears after section "Examples" but should be before it
- astropy/coordinates/sky_coordinate.py:1295:9: D420 Section "See Also" appears after section "Notes" but should be before it
- astropy/coordinates/sky_coordinate.py:1346:9: D420 Section "See Also" appears after section "Notes" but should be before it
- astropy/coordinates/sky_coordinate.py:1405:9: D420 Section "See Also" appears after section "Notes" but should be before it
... 102 additional changes omitted for project

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

Changes by rule (2 rules affected)

code total + violation - violation + fix - fix
D420 508 0 508 0 0
DOC501 4 2 2 0 0

User added 2 commits February 23, 2026 10:19
… a string

A '#' character is only a comment when it appears outside of any nested
string literal in the interpolation. For example, f"{'#'}" is valid
Python 3.11 — the '#' is inside a string, not a comment.

Add has_comment_hash() helper that scans interpolation text tracking
string nesting depth to distinguish comment '#' from '#' in strings.

Add test cases for '#' inside nested string literals.
Copy link
Contributor Author

@stakeswky stakeswky left a comment

Choose a reason for hiding this comment

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

Good catch, thanks! You're right — f"{'#'}" is perfectly valid in Python 3.11 because the # is inside a nested string literal, not a comment.

I've added a has_comment_hash() helper that scans the interpolation text while tracking string nesting depth, so it only returns true when # appears outside of any nested string. The simple interpolation_text.contains('#') check has been replaced with this.

Also added test cases for {'#'} and {"#"} in the fixture to cover this.

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.

Looks like there are some compilation errors, and I had one question/concern about the implementation itself while I was here.

User and others added 4 commits February 25, 2026 06:34
-  has  in the format spec, not a comment; only scan the
  expression part (before the format spec) for comment hashes
- Merge upstream/main to resolve Cargo.lock dependency conflicts
- Add test cases for hash-in-format-spec (e.g. , )
…ash/comment check

Instead of manually scanning interpolation text for backslashes and comment
hashes (which failed for nested cases like '{1:{x #}}'), delegate to the
parser's own PEP 701 detection via unsupported_syntax_errors().

- Removes has_comment_hash() and the per-element text scanning
- Handles all nesting levels correctly (nested interpolations in format specs)
- Adds test case for '{1:{x #}}' (comment in nested format-spec interpolation)
Address review feedback: match all Pep701FString variants (Backslash,
Comment, NestedQuote) using a wildcard instead of importing the private
FStringKind enum. This fixes the compilation error from importing
ruff_python_parser::error::FStringKind (private module) and also covers
the NestedQuote variant which was previously missed.
@stakeswky
Copy link
Contributor Author

Thanks for the review @ntBre! I've pushed a fix:

  • Removed the import of the private ruff_python_parser::error::FStringKind (which caused the compilation error).
  • Changed the match to UnsupportedSyntaxErrorKind::Pep701FString(_) — this uses a wildcard to cover all PEP 701 f-string variants (Backslash, Comment, and NestedQuote), so we no longer need to reference the private enum and we also correctly handle the nested-quote case that was previously missed.

Compilation passes cleanly 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.

Thank you!

A couple of the new test cases still aren't emitting diagnostics, but I think that's related to #23360 and not to any of the changes in this PR, since I see the same behavior on main.

@ntBre ntBre added bug Something isn't working fixes Related to suggested fixes for violations labels Feb 25, 2026
@ntBre ntBre changed the title fix(RUF027): suppress fix when interpolation contains comments on Python < 3.12 [ruff] Suppress diagnostic for invalid f-strings before Python 3.12 (RUF027) Feb 25, 2026
@ntBre ntBre added the preview Related to preview mode features label Feb 25, 2026
@ntBre ntBre merged commit 728609a into astral-sh:main Feb 25, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fixes Related to suggested fixes for violations preview Related to preview mode features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RUF027 creates invalid f-string interpolations with comments before Python 3.12

3 participants