Skip to content

[pyflakes] Report duplicate imports in typing.TYPE_CHECKING block (F811)#22560

Merged
ntBre merged 38 commits into
astral-sh:mainfrom
chirizxc:F811
May 22, 2026
Merged

[pyflakes] Report duplicate imports in typing.TYPE_CHECKING block (F811)#22560
ntBre merged 38 commits into
astral-sh:mainfrom
chirizxc:F811

Conversation

@chirizxc

Copy link
Copy Markdown
Contributor

Summary

See #22554

Test Plan

cargo nextest run pyflake

@chirizxc

Copy link
Copy Markdown
Contributor Author

This behavior makes it closer to the original pyflakes.

@chirizxc chirizxc changed the title [pyflakes] Report more diagnostic for F811 [pyflakes] Report duplicate imports in typing.TYPE_CHECKING block F811 Jan 13, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jan 13, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+53 -0 violations, +0 -0 fixes in 13 projects; 43 projects unchanged)

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

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

+ disnake/components.py:34:24: F811 [*] Redefinition of unused `TypeAlias` from line 13: `TypeAlias` redefined here
+ disnake/types/interactions.py:20:24: F811 [*] Redefinition of unused `TypeAlias` from line 5: `TypeAlias` redefined here
+ disnake/ui/_types.py:9:24: F811 [*] Redefinition of unused `TypeAlias` from line 6: `TypeAlias` redefined here

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

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

+ airflow-core/src/airflow/models/dag.py:80:40: F811 [*] Redefinition of unused `relativedelta` from line 28: `relativedelta` redefined here
+ airflow-core/src/airflow/models/taskinstance.py:118:26: F811 [*] Redefinition of unused `datetime` from line 28: `datetime` redefined here
+ airflow-core/src/airflow/models/taskreschedule.py:39:12: F811 [*] Redefinition of unused `datetime` from line 22: `datetime` redefined here
+ airflow-core/src/airflow/serialization/serialized_objects.py:128:41: F811 [*] Redefinition of unused `DagRunInfo` from line 108: `DagRunInfo` redefined here
+ airflow-core/src/airflow/serialization/serialized_objects.py:128:53: F811 [*] Redefinition of unused `Timetable` from line 108: `Timetable` redefined here
+ providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py:63:40: F811 [*] Redefinition of unused `CLICommand` from line 30: `CLICommand` redefined here
+ providers/amazon/src/airflow/providers/amazon/aws/triggers/dms.py:26:61: F811 [*] Redefinition of unused `AwsGenericHook` from line 21: `AwsGenericHook` redefined here
+ providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/hooks/kubernetes.py:53:33: F811 [*] Redefinition of unused `AsyncGenerator` from line 23: `AsyncGenerator` redefined here
+ providers/databricks/src/airflow/providers/databricks/operators/databricks.py:58:9: F811 [*] Redefinition of unused `DatabricksWorkflowTaskGroup` from line 36: `DatabricksWorkflowTaskGroup` redefined here
+ providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py:76:40: F811 [*] Redefinition of unused `CLICommand` from line 45: `CLICommand` redefined here
... 1 additional changes omitted for project

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

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

+ superset/utils/webdriver.py:56:24: F811 [*] Redefinition of unused `Any` from line 24: `Any` redefined here

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

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

+ ibis/backends/polars/__init__.py:25:33: F811 [*] Redefinition of unused `Iterable` from line 3: `Iterable` redefined here
+ ibis/backends/sqlite/__init__.py:33:12: F811 [*] Redefinition of unused `sqlite3` from line 5: `sqlite3` redefined here

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

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

+ libs/core/langchain_core/utils/uuid.py:15:22: F811 [*] Redefinition of unused `UUID` from line 10: `UUID` redefined here
+ libs/core/tests/unit_tests/language_models/chat_models/test_base.py:61:51: F811 [*] Redefinition of unused `LLMResult` from line 45: `LLMResult` redefined here
+ libs/langchain_v1/langchain/agents/middleware/_execution.py:52:33: F811 [*] Redefinition of unused `Mapping` from line 12: `Mapping` redefined here
+ libs/langchain_v1/langchain/agents/middleware/_execution.py:52:42: F811 [*] Redefinition of unused `Sequence` from line 12: `Sequence` redefined here
+ libs/langchain_v1/langchain/agents/middleware/_execution.py:53:25: F811 [*] Redefinition of unused `Path` from line 14: `Path` redefined here
+ libs/langchain_v1/langchain/agents/middleware/types.py:20:33: F811 [*] Redefinition of unused `Awaitable` from line 5: `Awaitable` redefined here
+ libs/langchain_v1/tests/unit_tests/agents/middleware/implementations/test_shell_execution_policies.py:22:25: F811 [*] Redefinition of unused `Path` from line 7: `Path` redefined here

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

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

+ pandas/core/resample.py:114:37: F811 [*] Redefinition of unused `NDFrame` from line 51: `NDFrame` redefined here

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

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

+ src/integrations/prefect-aws/prefect_aws/workers/ecs_worker.py:97:22: F811 [*] Redefinition of unused `UUID` from line 64: `UUID` redefined here
+ src/integrations/prefect-docker/prefect_docker/worker.py:89:9: F811 [*] Redefinition of unused `FlowRun` from line 68: `FlowRun` redefined here
+ src/prefect/flow_runs.py:53:46: F811 [*] Redefinition of unused `PrefectClient` from line 16: `PrefectClient` redefined here
+ src/prefect/flows.py:137:48: F811 [*] Redefinition of unused `FlowRun` from line 58: `FlowRun` redefined here
+ src/prefect/tasks.py:88:33: F811 [*] Redefinition of unused `TaskRunContext` from line 60: `TaskRunContext` redefined here
+ tests/deployment/test_flow_runs.py:31:46: F811 [*] Redefinition of unused `PrefectClient` from line 14: `PrefectClient` redefined here

pypa/pip (+1 -0 violations, +0 -0 fixes)

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

+ tests/unit/resolution_resolvelib/test_provider.py:27:58: F811 [*] Redefinition of unused `Candidate` from line 12: `Candidate` redefined here

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

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

+ tests/units/test_app.py:175:38: F811 [*] Redefinition of unused `AuthProvider` from line 37: `AuthProvider` redefined here

rotki/rotki (+4 -0 violations, +0 -0 fixes)

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

+ rotkehlchen/chain/ethereum/modules/sushiswap/decoder.py:39:65: F811 [*] Redefinition of unused `EvmEvent` from line 30: `EvmEvent` redefined here
+ rotkehlchen/chain/evm/decoding/decoder.py:106:50: F811 [*] Redefinition of unused `CounterpartyDetails` from line 24: `CounterpartyDetails` redefined here
+ rotkehlchen/chain/evm/decoding/spark/decoder.py:9:50: F811 [*] Redefinition of unused `CounterpartyDetails` from line 3: `CounterpartyDetails` redefined here
+ rotkehlchen/tests/unit/decoders/test_stakedao.py:30:58: F811 [*] Redefinition of unused `EthereumInquirer` from line 13: `EthereumInquirer` redefined here

python-trio/trio (+5 -0 violations, +0 -0 fixes)

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

+ src/trio/_core/_run.py:78:12: F811 [*] Redefinition of unused `outcome` from line 28: `outcome` redefined here
+ src/trio/_core/_tests/test_ki.py:31:9: F811 [*] Redefinition of unused `AsyncIterator` from line 9: `AsyncIterator` redefined here
+ src/trio/_core/_tests/test_ki.py:32:9: F811 [*] Redefinition of unused `Callable` from line 9: `Callable` redefined here
+ src/trio/_core/_tests/test_ki.py:34:9: F811 [*] Redefinition of unused `Iterator` from line 9: `Iterator` redefined here
+ src/trio/_tests/tools/test_sync_requirements.py:20:25: F811 [*] Redefinition of unused `Path` from line 3: `Path` redefined here

wntrblm/nox (+6 -0 violations, +0 -0 fixes)

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

+ nox/_option_set.py:33:33: F811 [*] Redefinition of unused `Callable` from line 25: `Callable` redefined here
+ nox/_option_set.py:33:43: F811 [*] Redefinition of unused `Iterable` from line 25: `Iterable` redefined here
+ nox/command.py:29:33: F811 [*] Redefinition of unused `Iterable` from line 22: `Iterable` redefined here
+ nox/command.py:29:43: F811 [*] Redefinition of unused `Mapping` from line 22: `Mapping` redefined here
+ nox/command.py:29:52: F811 [*] Redefinition of unused `Sequence` from line 22: `Sequence` redefined here
+ tests/test_manifest.py:36:33: F811 [*] Redefinition of unused `Sequence` from line 18: `Sequence` redefined here

pdm-project/pdm (+5 -0 violations, +0 -0 fixes)

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

+ src/pdm/cli/commands/python.py:20:26: F811 [*] Redefinition of unused `ArgumentParser` from line 7: `ArgumentParser` redefined here
+ src/pdm/installers/installers.py:28:35: F811 [*] Redefinition of unused `WheelContentElement` from line 16: `WheelContentElement` redefined here
+ src/pdm/models/reporter.py:15:31: F811 [*] Redefinition of unused `Progress` from line 9: `Progress` redefined here
+ src/pdm/resolver/providers.py:37:57: F811 [*] Redefinition of unused `LockedRepository` from line 17: `LockedRepository` redefined here
+ src/pdm/resolver/providers.py:38:41: F811 [*] Redefinition of unused `Requirement` from line 18: `Requirement` redefined here

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
F811 53 53 0 0 0

@MichaReiser MichaReiser added the rule Implementing or modifying a lint rule label Jan 14, 2026
@chirizxc

Copy link
Copy Markdown
Contributor Author

I'm not sure how much this can be considered a false positive, but there is such code in mypy:

@chirizxc

Copy link
Copy Markdown
Contributor Author

Should the fix report mention that one of the imports is a duplicate and should be removed based on the code context? (instead of format!("Remove runtime {name} import")

Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
chirizxc and others added 3 commits February 11, 2026 13:36
…used.rs

Co-authored-by: Amethyst Reese <amethyst@n7.gg>
…used.rs

Co-authored-by: Amethyst Reese <amethyst@n7.gg>

@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! And sorry for the delay in review. Will this close #17555 and #12270 too?

This looks reasonable to me overall, I just had some smaller suggestions. And I think we should make this a preview change even though it's arguably a bug fix. It's still a fairly large expansion of an existing rule in practice.

Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
@chirizxc

Copy link
Copy Markdown
Contributor Author

Thank you! And sorry for the delay in review. Will this close #17555 and #12270 too?

This looks reasonable to me overall, I just had some smaller suggestions. And I think we should make this a preview change even though it's arguably a bug fix. It's still a fairly large expansion of an existing rule in practice.

I think it should, but I also saw a false positive in mypy and some other project, but there it already said noqa: F401, so I think this is about the expected behavior.

@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 again. I took a deeper look here today, and it seems that there is some extraneous, or at least untested, code here. I'm also not sure we should be treating TYPE_CHECKING duplicates as so different from the other cases the rule covers.

Comment thread crates/ruff_linter/src/rules/pyflakes/mod.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/resources/test/fixtures/pyflakes/F811_33.py Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
Comment thread crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs Outdated
@ntBre ntBre added the preview Related to preview mode features label Feb 23, 2026
ntBre added a commit to ntBre/mypy that referenced this pull request Feb 23, 2026
Summary
--

While reviewing astral-sh/ruff#22560, which expands our `redefined-while-unused`
rule to catch re-imports in `TYPE_CHECKING` blocks, we noticed a new diagnostic
on this code. When I checked the blame and noticed that the comment was 8 years
old, I thought it might be worth trying to remove the import. I'm very happy to
close this if I'm missing something, but the tests seem to be passing locally,
so I figured I'd open a PR!

Test Plan
--

I set up my environment based on the contributing guide and then ran:

```shell
python runtests.py
```
hauntsaninja pushed a commit to python/mypy that referenced this pull request Feb 24, 2026
While reviewing astral-sh/ruff#22560, which expands our
`redefined-while-unused` rule to catch re-imports in `TYPE_CHECKING`
blocks, we noticed a new diagnostic on this code. When I checked the
blame and noticed that the comment was 8 years old, I thought it might
be worth trying to remove the import. I'm very happy to close this if
I'm missing something, but the tests seem to be passing locally, so I
figured I'd open a PR!
@chirizxc

chirizxc commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

Is it worth adding code with TYPE_CHEKING as an example?

@chirizxc

chirizxc commented Mar 1, 2026

Copy link
Copy Markdown
Contributor Author

Is it worth adding code with TYPE_CHEKING as an example?

I still have this question, and I think adding an example to the documentation would be useful when people see that the ruff update has started detecting new cases F811.

@ntBre

ntBre commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Sure, I think it's fine to add another example.

@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 to me overall. I just had one more question about ecosystem results.

I was looking at the pandas case you mentioned here again today, and I think we may be able to handle this by using the SemanticModel::dominates method. I asked Claude to look into this and it came up with this patch:

diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_34.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_34.py
index 7755851b7a..432539d675 100644
--- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_34.py
+++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_34.py
@@ -13,4 +13,15 @@ if TYPE_CHECKING:
 	import pyarrow_hotfix
 
 def foo():
-	import pyarrow_hotfix
\ No newline at end of file
+	import pyarrow_hotfix
+
+# Should NOT detect: the runtime import is itself conditional, so the
+# TYPE_CHECKING import is not redundant (e.g., needed for type checkers
+# when the condition is False).
+HAS_THING = True
+
+if HAS_THING:
+    from foo import Bar
+
+if TYPE_CHECKING:
+    from foo import Bar
\ No newline at end of file
diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs
index fdba0a65ba..f33e0bef4e 100644
--- a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs
+++ b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs
@@ -303,7 +303,12 @@ fn bindings_in_different_forks(
                 (left_binding.as_any_import(), right_binding.as_any_import())
             && left_import.qualified_name() == right_import.qualified_name()
         {
-            return false;
+            // Only report if the non-`TYPE_CHECKING` import unconditionally
+            // precedes the `TYPE_CHECKING` import. If the runtime import is
+            // itself inside a conditional block (e.g., `if HAS_PYARROW:`),
+            // the `TYPE_CHECKING` import is not redundant.
+            let (non_tc, tc) = if right_ { (left, right) } else { (right, left) };
+            return !checker.semantic().dominates(non_tc, tc);
         }
     }

The comment is rather verbose and the non_tc and tc names aren't the best, but it seems to resolve the false positive in a fairly neat way. What do you think?

I was kind of hoping that dominates would simplify bindings_in_different_forks in general, but I think that's only the case if we want to treat other conditional branches like we're treating TYPE_CHECKING here.

@chirizxc

chirizxc commented Mar 3, 2026

Copy link
Copy Markdown
Contributor Author

The comment is rather verbose and the non_tc and tc names aren't the best, but it seems to resolve the false positive in a fairly neat way. What do you think?

Yes, I think that would be more logical. I will change the names to make them more readable.

@ntBre

ntBre commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Nice, that looks like an ecosystem improvement. I am still seeing one more potentially wrong case (well two instances of the same issue):

I feel like we may have talked about this earlier, so sorry for the duplication if so, but these are cases like:

if TYPE_CHECKING:
	import foo

import foo  # F811

where the runtime import is after the type checking import.

I think we probably shouldn't emit a diagnostic in this case, or at least we should add a test case verifying that we suggest removing the TYPE_CHECKING import rather than the runtime import. If our fix removes the runtime import instead, it will obviously break the code.

I went through all of the other ecosystem checks, though, and they look good to me.

@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! And sorry for the delay. I pushed a merge commit handling the conflicts that popped up since my last review.

@MichaReiser

Copy link
Copy Markdown
Member

@ntBre do we want to merge this?

@ntBre

ntBre commented May 22, 2026

Copy link
Copy Markdown
Contributor

Yep! Just taking one more look at the ecosystem report. There's a new diagnostic on reflex and on airflow, but they both look like true positives :)

@ntBre ntBre changed the title [pyflakes] Report duplicate imports in typing.TYPE_CHECKING block F811 [pyflakes] Report duplicate imports in typing.TYPE_CHECKING block (F811) May 22, 2026
@ntBre ntBre merged commit 928018c into astral-sh:main May 22, 2026
45 checks passed
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
… (`F811`) (astral-sh#22560)

## Summary

See astral-sh#22554

## Test Plan

`cargo nextest run pyflake`

---------

Co-authored-by: Amethyst Reese <amethyst@n7.gg>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
@chirizxc chirizxc deleted the F811 branch May 25, 2026 03:03
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
… (`F811`) (astral-sh#22560)

## Summary

See astral-sh#22554

## Test Plan

`cargo nextest run pyflake`

---------

Co-authored-by: Amethyst Reese <amethyst@n7.gg>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
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.

4 participants