Skip to content

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

Open
chirizxc wants to merge 37 commits intoastral-sh:mainfrom
chirizxc:F811
Open

[pyflakes] Report duplicate imports in typing.TYPE_CHECKING block F811#22560
chirizxc wants to merge 37 commits intoastral-sh:mainfrom
chirizxc:F811

Conversation

@chirizxc
Copy link
Contributor

Summary

See #22554

Test Plan

cargo nextest run pyflake

@chirizxc
Copy link
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
Copy link

astral-sh-bot bot commented Jan 13, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+51 -0 violations, +0 -0 fixes in 12 projects; 44 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 (+10 -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:108:26: F811 [*] Redefinition of unused `datetime` from line 27: `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:62: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:80:40: F811 [*] Redefinition of unused `CLICommand` from line 45: `CLICommand` redefined here

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:47:51: F811 [*] Redefinition of unused `LLMResult` from line 33: `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:91:22: F811 [*] Redefinition of unused `UUID` from line 65: `UUID` redefined here
+ src/integrations/prefect-docker/prefect_docker/worker.py:74:9: F811 [*] Redefinition of unused `FlowRun` from line 54: `FlowRun` redefined here
+ src/prefect/flow_runs.py:52:46: F811 [*] Redefinition of unused `PrefectClient` from line 15: `PrefectClient` redefined here
+ src/prefect/flows.py:131:48: F811 [*] Redefinition of unused `FlowRun` from line 54: `FlowRun` redefined here
+ src/prefect/tasks.py:87:33: F811 [*] Redefinition of unused `TaskRunContext` from line 59: `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:24:58: F811 [*] Redefinition of unused `Candidate` from line 12: `Candidate` 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:35:65: F811 [*] Redefinition of unused `EvmEvent` from line 26: `EvmEvent` redefined here
+ rotkehlchen/chain/evm/decoding/decoder.py:103: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:24:35: F811 [*] Redefinition of unused `WheelContentElement` from line 15: `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:34:57: F811 [*] Redefinition of unused `LockedRepository` from line 15: `LockedRepository` redefined here
+ src/pdm/resolver/providers.py:35:41: F811 [*] Redefinition of unused `Requirement` from line 16: `Requirement` redefined here

Changes by rule (1 rules affected)

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

@MichaReiser MichaReiser added the rule Implementing or modifying a lint rule label Jan 14, 2026
@chirizxc
Copy link
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
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")

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>
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 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.

@chirizxc
Copy link
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.

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 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.

@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
Copy link
Contributor Author

chirizxc commented Feb 24, 2026

Is it worth adding code with TYPE_CHEKING as an example?

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! All but one of my comments are resolved, I just had two more very small nits. I am still curious about the second preview check, though.

I also noticed one potential issue in the ecosystem check.

@chirizxc

This comment was marked as outdated.

@chirizxc
Copy link
Contributor Author

chirizxc commented Mar 1, 2026

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
Copy link
Contributor

ntBre commented Mar 2, 2026

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

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, 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
Copy link
Contributor Author

chirizxc commented Mar 3, 2026

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
Copy link
Contributor

ntBre commented Mar 3, 2026

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.

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