Visitar URL original
Fix F401 infinite loop when adding duplicate symbols to __all__ by jonathan-fulton · Pull Request #23052 · astral-sh/ruff · GitHub
Skip to content

Fix F401 infinite loop when adding duplicate symbols to __all__ - #23052

Closed
jonathan-fulton wants to merge 1 commit into
astral-sh:mainfrom
jonathan-fulton:fix/f401-infinite-loop
Closed

jonathan-fulton wants to merge 1 commit into
astral-sh:mainfrom
jonathan-fulton:fix/f401-infinite-loop

Conversation

@jonathan-fulton

Copy link
Copy Markdown

Summary

Fixes #22221

Running ruff check --fix could enter an infinite loop when fixing F401 (unused imports) in __init__.py files that combine star imports with explicit imports of the same symbol.

Problem

When adding symbols to __all__ in fix_by_reexporting, duplicate symbols could be collected (e.g., when a module has both from foo import * and from foo import bar). Without deduplication, ruff would repeatedly add the same symbol to __all__, creating an infinite fix loop.

Changes

Added a dedup() call after sort_unstable() in fix_by_reexporting to remove duplicate symbols before adding them to __all__.

Test Plan

Tested with:

# __init__.py
from .foo import *
from .foo import bar

__all__ = []

Before this fix, ruff check --fix --unsafe-fixes would loop infinitely. After this fix, it correctly adds each symbol once.

Adds dedup() call after sorting imports in fix_by_reexporting to prevent
infinite loops when the same symbol appears multiple times due to star
imports combined with explicit imports.

Without deduplication, ruff would repeatedly add the same symbol to
__all__, creating an infinite fix loop.

Fixes astral-sh#22221
@dylwil3

dylwil3 commented Feb 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks for your interest in contributing to Ruff! Unfortunately this does not seem to fix the linked issue:

❯ gh pr checkout 23052

❯ mkdir foo && echo 'import foo.bar.module
import foo.baz.module

__all__ = ["foo"]' > foo/__init__.py

❯ cargo run -p ruff -- check --no-cache --isolated --select F401 --preview foo/__init__.py
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/ruff check --no-cache --isolated --select F401 --preview foo/__init__.py`
F401 [*] `foo.baz.module` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
 --> foo/__init__.py:2:8
  |
1 | import foo.bar.module
2 | import foo.baz.module
  |        ^^^^^^^^^^^^^^
3 |
4 | __all__ = ["foo"]
  |
help: Add `foo` to __all__
1 | import foo.bar.module
2 | import foo.baz.module
3 |
  - __all__ = ["foo"]
4 + __all__ = ["foo", "foo"]

Found 1 error.
[*] 1 fixable with the `--fix` option.

Moreover, even if it did, we need to do more than just change the autofix - the help text is also incorrect in this situation. So I think we need something closer to what I started working on in this draft #23038

Finally, I'm not sure I understand the test case you tried. The autofix would add bar not foo, and there are no duplicates or infinite loops created (before or after this PR).

@dylwil3 dylwil3 closed this Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

F401 fix causes infinite loop adding duplicates to __all__

2 participants