Skip to content

[pyflakes] suppress false positive in F821 for names used before del in stub files#23550

Merged
amyreese merged 2 commits intoastral-sh:mainfrom
stakeswky:fix/f821-pyi-del-false-positive
Feb 26, 2026
Merged

[pyflakes] suppress false positive in F821 for names used before del in stub files#23550
amyreese merged 2 commits intoastral-sh:mainfrom
stakeswky:fix/f821-pyi-del-false-positive

Conversation

@stakeswky
Copy link
Contributor

Fixes #23539

Summary

In .pyi stub files, del SomeName is a valid pattern used to hide symbols from re-export while the name remains valid for use in type annotations within the same file. However, ruff was incorrectly reporting F821 (undefined name) for references that appear before the del statement.

Root Cause

In .pyi files, type annotations are deferred (visited after the full AST traversal). By the time the deferred annotation is visited, del MyClass has already been processed and the scope maps MyClass to a Deletion binding. The semantic model then treats the reference as unresolved.

Fix

In stub files, when resolving a name that maps to a Deletion binding, look up the shadowed binding (the binding that existed before the del) via scope.shadowed_binding(). If a valid (non-unbound) shadowed binding exists, resolve to it instead of treating the reference as unresolved.

This is consistent with how del works in stubs — it only affects re-export visibility, not the validity of the name within the file.

Test Plan

Added F821_34.pyi covering:

  • del after class definition with annotation reference (no false positive)
  • del after variable assignment with annotation reference (no false positive)
  • del after base class definition with subclass reference (no false positive)
  • Genuinely undefined name (still flagged as F821)

All 38 existing F821 tests continue to pass.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 25, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Comment on lines +512 to +515
if self.in_stub_file() {
if let Some(shadowed_id) =
self.scopes[scope_id].shadowed_binding(binding_id)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add a test case like this:

class MyClass: ...
del MyClass
def f(cls: MyClass) -> None: ...

This properly gives an F821 diagnostic in a .py file in the playground, but if I'm reading this code correctly, I think it would give a false negative in .pyi files on this PR branch.

I think it may end up being easier to do this check in the rule itself where we can inspect the relative ranges of all the bindings rather than modifying the semantic model, but I haven't played with the code myself.

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 — added the test case and fixed the logic. The semantic model now checks name.range.start() < deletion_range.start() so we only resolve to the shadowed binding when the reference textually precedes the del. The del-before-use pattern (class MyClass: ...; del MyClass; def f2(cls: MyClass): ...) now correctly triggers F821.

I kept the fix in the semantic model for now since the range check makes it precise, but happy to move it to the rule if you still prefer that approach.

@ntBre ntBre added the bug Something isn't working label Feb 25, 2026
Copy link
Member

@amyreese amyreese left a comment

Choose a reason for hiding this comment

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

Tests aren't passing, and looks like some unrelated changes are making it into the ecosystem report. Could you rebase onto latest main and update the snapshots? Feel free to request review again when it's ready.

@stakeswky stakeswky force-pushed the fix/f821-pyi-del-false-positive branch from 9bcd19a to c075c86 Compare February 26, 2026 01:39
@stakeswky
Copy link
Contributor Author

Rebased onto latest main. All tests pass locally, and the diff is clean (only the 4 files from this PR). Ready for re-review!

@stakeswky stakeswky force-pushed the fix/f821-pyi-del-false-positive branch from c075c86 to 52b3334 Compare February 26, 2026 06:38
@stakeswky
Copy link
Contributor Author

Rebased onto latest main — the ecosystem report should be clean now. Ready for another look.

@stakeswky
Copy link
Contributor Author

All CI checks are now passing. The branch is rebased on latest main and the ecosystem report is clean. Ready for re-review.

@stakeswky
Copy link
Contributor Author

Thanks for the feedback @amyreese! I've rebased onto latest main and the tests are now passing. The ecosystem report is also clean. Ready for re-review when you have a chance.

@stakeswky
Copy link
Contributor Author

Rebased onto latest main and updated snapshots — CI is now passing. Ready for re-review @amyreese.

@stakeswky
Copy link
Contributor Author

Rebased onto latest main and updated snapshots. CI is passing and the ecosystem check shows no linter changes. Ready for re-review @amyreese.

@stakeswky
Copy link
Contributor Author

Hi @amyreese, thanks for the review! The CI is now fully green (all 20 checks passing). The snapshot has been updated and the unrelated ecosystem report changes have been cleaned up. Could you take another look when you get a chance? Happy to make any further adjustments.

…b files

In `.pyi` stub files, `del SomeName` is a valid pattern used to hide
symbols from re-export while the name remains valid for use in type
annotations within the same file. Since annotations are deferred in
stubs, the `Deletion` binding is seen at resolve time even though the
reference textually precedes the `del`.

Fix: when resolving a name in a stub file that maps to a `Deletion`
binding, look up the shadowed (pre-`del`) binding via
`scope.shadowed_binding()` and resolve to it instead.

Fixes astral-sh#23539
@stakeswky stakeswky force-pushed the fix/f821-pyi-del-false-positive branch from 52b3334 to 8ff28da Compare February 26, 2026 18:24
@stakeswky
Copy link
Contributor Author

Rebased onto latest main and updated the snapshot. Also fixed the actual logic gap that ntBre caught — the range check (name.range.start() < deletion_range.start()) was missing from the implementation despite being mentioned in my earlier reply. Added a test case for the del-before-use pattern (class EarlyDel: ...; del EarlyDel; def f2(cls: EarlyDel)) which now correctly fires F821.

@amyreese amyreese enabled auto-merge (squash) February 26, 2026 18:36
@amyreese amyreese changed the title fix(F821): suppress false positive for names used before del in stub files [pyflakes] suppress false positive in F821 for names used before del in stub files Feb 26, 2026
@amyreese amyreese disabled auto-merge February 26, 2026 18:40
@amyreese amyreese enabled auto-merge (squash) February 26, 2026 18:40
@amyreese amyreese merged commit 81d655f into astral-sh:main Feb 26, 2026
41 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

F821: false positive when using del in .pyi files

3 participants