[flake8-type-checking] Avoid strict behavior when future-annotations are enabled (TC001, TC002, TC003)#25035
Merged
ntBre merged 2 commits intoMay 15, 2026
Conversation
…02/TC003 (fixes astral-sh#23185) Root cause: the skip condition for implicit runtime imports in typing_only_runtime_import required BOTH !future_annotations AND !strict to be true. When a user enabled future_annotations=true without enabling strict mode, the first clause short-circuited the skip, causing TC001/TC002/TC003 to flag imports that have valid runtime siblings -- silently flipping the rules into strict-mode behavior. Fix: gate the skip on the strict setting only. future_annotations is orthogonal -- it controls whether 'from __future__ import annotations' is auto-inserted as part of the fix, not whether runtime-shadowed imports are flagged. Matches the intent stated in the docs and the direction suggested by the maintainer on the issue.
ntBre
reviewed
May 13, 2026
ntBre
left a comment
Contributor
There was a problem hiding this comment.
I had a couple of nits, but this looks right to me, thanks!
…re/test docstrings Per @ntBre's review on astral-sh#25035: - Restore the simpler pre-astral-sh#19100 comment in typing_only_runtime_import.rs - Use assert_diagnostics! for the regression test so future changes produce a snapshot diff instead of a hard assert failure - Drop the duplicated explanation from the test docstring and fixture comments; the test name + fixture header are sufficient context
Contributor
Author
|
Pushed
Full |
future_annotations incorrectly enables strict-mode behavior for TC001/TC002/TC003 (fixes #23185)flake8-type-checking] Avoid strict behavior when future-annotations are enabled (TC001, TC002, TC003)
thejchap
pushed a commit
to thejchap/ruff
that referenced
this pull request
May 23, 2026
…ions` are enabled (`TC001`, `TC002`, `TC003`) (astral-sh#25035) ## Summary Fixes astral-sh#23185. Enabling `lint.future-annotations = true` was silently flipping `TC001`/`TC002`/`TC003` into strict-mode behaviour, even when `lint.flake8-type-checking.strict = false`. The two settings are orthogonal — the former controls whether `from __future__ import annotations` is auto-inserted as a fix; the latter controls whether typing-only imports whose module is already imported at runtime should be flagged. ## Root cause In [`crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs), the skip condition for implicit runtime imports read: ```rust if !checker.settings().future_annotations && !checker.settings().flake8_type_checking.strict && runtime_imports.iter().any(|import| is_implicit_import(binding, import)) { continue; } ``` This required **both** `!future_annotations` **and** `!strict` to be true. The moment a user opted into `future_annotations = true`, the first clause became false and the entire skip was bypassed — making the rules behave as if `strict = true`, regardless of the user's explicit `strict = false` setting. This also matches the direction @ntBre suggested on the issue: > This does sound like a bug to me. I think I may have gotten this logic wrong, and it should be an `or` instead of `and`… ## Solution Drop the `!future_annotations` clause. Only `!strict` should gate whether implicit runtime imports are skipped. The `future_annotations` setting is handled elsewhere (as part of the fix-generation path for adding the `__future__` import); it has no bearing on which imports are flagged. ## Testing - **New regression test** in `crates/ruff_linter/src/rules/flake8_type_checking/mod.rs`: `future_annotations_respects_non_strict_mode`, backed by a new fixture `crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC001-3_future_strict.py`. The test pins the exact scenario from the issue (`future_annotations=true`, `strict=false`, typing-only import whose module has a runtime-used sibling) and asserts zero `TC001`/`TC002`/`TC003` diagnostics are produced — a result that would have been non-zero prior to this patch. - All 92 pre-existing `flake8_type_checking` lib tests still pass locally; the `add_future_import` snapshot tests (which exercise the `future_annotations = true` path) also pass unchanged, confirming the fix does not regress the auto-insert-`__future__`-import behaviour. - `cargo test --lib flake8_type_checking` → 92 passed, 0 failed - `cargo test --lib add_future_import` → 12 passed, 0 failed ## Checklist - [x] Fixes the root cause (not just the symptom) - [x] New test covers the exact failing scenario from the issue - [x] All existing tests pass - [x] No unrelated changes - [x] Code style matches project conventions - [x] Followed CONTRIBUTING.md
anishgirianish
pushed a commit
to anishgirianish/ruff
that referenced
this pull request
May 28, 2026
…ions` are enabled (`TC001`, `TC002`, `TC003`) (astral-sh#25035) ## Summary Fixes astral-sh#23185. Enabling `lint.future-annotations = true` was silently flipping `TC001`/`TC002`/`TC003` into strict-mode behaviour, even when `lint.flake8-type-checking.strict = false`. The two settings are orthogonal — the former controls whether `from __future__ import annotations` is auto-inserted as a fix; the latter controls whether typing-only imports whose module is already imported at runtime should be flagged. ## Root cause In [`crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs), the skip condition for implicit runtime imports read: ```rust if !checker.settings().future_annotations && !checker.settings().flake8_type_checking.strict && runtime_imports.iter().any(|import| is_implicit_import(binding, import)) { continue; } ``` This required **both** `!future_annotations` **and** `!strict` to be true. The moment a user opted into `future_annotations = true`, the first clause became false and the entire skip was bypassed — making the rules behave as if `strict = true`, regardless of the user's explicit `strict = false` setting. This also matches the direction @ntBre suggested on the issue: > This does sound like a bug to me. I think I may have gotten this logic wrong, and it should be an `or` instead of `and`… ## Solution Drop the `!future_annotations` clause. Only `!strict` should gate whether implicit runtime imports are skipped. The `future_annotations` setting is handled elsewhere (as part of the fix-generation path for adding the `__future__` import); it has no bearing on which imports are flagged. ## Testing - **New regression test** in `crates/ruff_linter/src/rules/flake8_type_checking/mod.rs`: `future_annotations_respects_non_strict_mode`, backed by a new fixture `crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC001-3_future_strict.py`. The test pins the exact scenario from the issue (`future_annotations=true`, `strict=false`, typing-only import whose module has a runtime-used sibling) and asserts zero `TC001`/`TC002`/`TC003` diagnostics are produced — a result that would have been non-zero prior to this patch. - All 92 pre-existing `flake8_type_checking` lib tests still pass locally; the `add_future_import` snapshot tests (which exercise the `future_annotations = true` path) also pass unchanged, confirming the fix does not regress the auto-insert-`__future__`-import behaviour. - `cargo test --lib flake8_type_checking` → 92 passed, 0 failed - `cargo test --lib add_future_import` → 12 passed, 0 failed ## Checklist - [x] Fixes the root cause (not just the symptom) - [x] New test covers the exact failing scenario from the issue - [x] All existing tests pass - [x] No unrelated changes - [x] Code style matches project conventions - [x] Followed CONTRIBUTING.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Fixes #23185.
Enabling
lint.future-annotations = truewas silently flippingTC001/TC002/TC003into strict-mode behaviour, even whenlint.flake8-type-checking.strict = false. The two settings are orthogonal — the former controls whetherfrom __future__ import annotationsis auto-inserted as a fix; the latter controls whether typing-only imports whose module is already imported at runtime should be flagged.Root cause
In
crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs, the skip condition for implicit runtime imports read:This required both
!future_annotationsand!strictto be true. The moment a user opted intofuture_annotations = true, the first clause became false and the entire skip was bypassed — making the rules behave as ifstrict = true, regardless of the user's explicitstrict = falsesetting.This also matches the direction @ntBre suggested on the issue:
Solution
Drop the
!future_annotationsclause. Only!strictshould gate whether implicit runtime imports are skipped. Thefuture_annotationssetting is handled elsewhere (as part of the fix-generation path for adding the__future__import); it has no bearing on which imports are flagged.Testing
crates/ruff_linter/src/rules/flake8_type_checking/mod.rs:future_annotations_respects_non_strict_mode, backed by a new fixturecrates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC001-3_future_strict.py. The test pins the exact scenario from the issue (future_annotations=true,strict=false, typing-only import whose module has a runtime-used sibling) and asserts zeroTC001/TC002/TC003diagnostics are produced — a result that would have been non-zero prior to this patch.flake8_type_checkinglib tests still pass locally; theadd_future_importsnapshot tests (which exercise thefuture_annotations = truepath) also pass unchanged, confirming the fix does not regress the auto-insert-__future__-import behaviour.cargo test --lib flake8_type_checking→ 92 passed, 0 failedcargo test --lib add_future_import→ 12 passed, 0 failedChecklist