Skip to content

Fix used-before-assignment false negative with bare type annotations#10852

Open
worksbyfriday wants to merge 7 commits intopylint-dev:mainfrom
worksbyfriday:fix-used-before-assignment-bare-annotation
Open

Fix used-before-assignment false negative with bare type annotations#10852
worksbyfriday wants to merge 7 commits intopylint-dev:mainfrom
worksbyfriday:fix-used-before-assignment-bare-annotation

Conversation

@worksbyfriday
Copy link
Copy Markdown
Contributor

Description

Fixes #10847

When a variable has a bare type annotation (e.g. err: int) without a value and is only assigned inside except blocks, pylint failed to report used-before-assignment.

Reproducer

def case():
    result = None
    err: int  # bare annotation, no value
    try:
        result = 1
    except Exception:
        err = 1
    if not result:
        print(err)  # should warn, but didn't

Removing the type annotation correctly triggers the warning. Adding a value (err: int = 0) correctly suppresses it.

Root cause

In NamesConsumer.get_next_to_consume(), the except-block assignments are correctly filtered into consumed_uncertain. However, the bare annotation's AssignName node remains in found_nodes — and since it appears before the usage in line order, _is_variable_violation() sets maybe_before_assign = False (usage line > definition line). The bare annotation is then treated as a valid definition, suppressing the check entirely.

Fix

After all uncertain-node filtering in get_next_to_consume(), if the remaining found_nodes consists only of bare annotations (AnnAssign without a value) and there are uncertain definitions in consumed_uncertain, filter out the bare annotations. This causes found_nodes to be empty, allowing the code to correctly reach _report_unfound_name_definition() which emits the used-before-assignment warning.

Test

Added two test cases to used_before_assignment_type_annotations.py:

  • bare_annotation_with_except_assignment() — expects used-before-assignment
  • bare_annotation_with_value_and_except() — no warning expected (annotation has a value)

All 872 functional tests pass (13 skipped). The one pre-existing failure (use_yield_from) is unrelated.

Type of Changes

  • Bug fix (false negative)

When a variable has a bare type annotation (e.g. `err: int`) and is
only assigned inside except blocks, pylint failed to report
used-before-assignment. The bare annotation was treated as a valid
definition, suppressing the check.

The fix filters bare annotations (AnnAssign without a value) from
found_nodes in get_next_to_consume() when there are uncertain
definitions (e.g., assignments in except blocks). This ensures the
code correctly falls through to _report_unfound_name_definition().

Closes pylint-dev#10847
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.03%. Comparing base (d2dc5df) to head (9391b02).
⚠️ Report is 51 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #10852      +/-   ##
==========================================
- Coverage   96.03%   96.03%   -0.01%     
==========================================
  Files         177      177              
  Lines       19621    19633      +12     
==========================================
+ Hits        18844    18855      +11     
- Misses        777      778       +1     
Files with missing lines Coverage Δ
pylint/checkers/variables.py 97.31% <100.00%> (+0.02%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Member

@Pierre-Sassoulas Pierre-Sassoulas 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 for working on pylint, let's optimize a little if we can.

# filter out assignments in the try portion, assuming they may fail
if found_nodes:
uncertain_nodes = (
self._uncertain_nodes_in_try_blocks_when_evaluating_except_blocks(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't this be filtered in self._uncertain_nodes_in_try_blocks_when_evaluating_except_blocks / _uncertain_nodes_if_tests instead ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. I considered both approaches. The _uncertain_nodes_* methods filter nodes that are conditionally defined (in try/except, if/else). But bare annotations are not conditionally defined — they are simply not definitions at all (no value assigned).

The reworked approach (commit b498124) moves bare annotations to consumed_uncertain instead of removing them from to_consume. This way: the variable checker knows the annotation exists (it is tracked, not deleted), but it is marked as uncertain, so used-before-assignment fires correctly when the name is used before any real assignment. The unused-variable checker does not false-positive on the annotation because it has been consumed.

This felt cleaner than adding annotation-detection logic to the _uncertain_nodes_* methods, which are specifically about control flow uncertainty, not type annotation semantics. But open to restructuring if you prefer the filtering approach.

@Pierre-Sassoulas Pierre-Sassoulas added the False Negative 🦋 No message is emitted but something is wrong with the code label Feb 19, 2026
@Pierre-Sassoulas Pierre-Sassoulas added this to the 4.1.0 milestone Feb 19, 2026
@Pierre-Sassoulas
Copy link
Copy Markdown
Member

The primer doesn't look good

@worksbyfriday
Copy link
Copy Markdown
Contributor Author

You're right on both counts. The primer regressions show my fix is too broad — filtering bare annotations in get_next_to_consume() affects more cases than intended.

Your suggestion to move the filtering to _uncertain_nodes_in_try_blocks_when_evaluating_except_blocks / _uncertain_nodes_if_tests makes sense — those methods are where uncertain nodes are classified, so that's where bare annotations should be identified as non-assignments. I'll look at those methods and see how to add bare annotation handling there instead.

Will push an updated approach once I've worked through it.

Instead of filtering bare type annotations out of found_nodes entirely
(which caused unused-variable false positives in the primer), move them
to consumed_uncertain. This preserves node tracking (preventing false
unused-variable) while still treating bare annotations as uncertain
definitions (correctly flagging used-before-assignment).

The key insight: removing nodes from found_nodes makes them invisible to
the unused-variable checker. Moving them to consumed_uncertain keeps them
tracked but marks them as conditionally defined.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@worksbyfriday
Copy link
Copy Markdown
Contributor Author

Pushed a rework based on the primer regression analysis. The key insight:

Before (caused unused-variable false positives): Bare annotations were removed from found_nodes, making them invisible to the unused-variable checker.

After: Bare annotations are moved to consumed_uncertain instead. This keeps them tracked (no false unused-variable) while treating them as uncertain definitions (correctly flagging used-before-assignment).

The change is in get_next_to_consume() — same location, but instead of filtering out bare annotations, we relocate them to consumed_uncertain. All 25 used_before_assignment functional tests and all 5 unused_variable functional tests pass locally.

@github-actions

This comment has been minimized.

@jacobtylerwalls jacobtylerwalls added the C: used-before-assignment Issues related to 'used-before-assignment' check label Feb 22, 2026
@jacobtylerwalls jacobtylerwalls self-requested a review February 22, 2026 00:34
Copy link
Copy Markdown
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

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

No concerns with the approach.

Comment thread tests/functional/u/used/used_before_assignment_type_annotations.py Outdated
Rewrite test examples per review: use a specific exception type
instead of Exception to avoid broad-exception-caught disables.
@github-actions

This comment has been minimized.

Use realistic int(text) conversion instead of bare assignments,
and TypeError/OverflowError instead of ValueError.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@github-actions

This comment has been minimized.

Rewrite except clauses to catch ValueError (the exception int()
actually raises for bad input) instead of (TypeError, OverflowError).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@worksbyfriday
Copy link
Copy Markdown
Contributor Author

Updated — switched to catching ValueError (which int() actually raises for bad input) instead of (TypeError, OverflowError).

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

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

Pandas example in the primer comment is a new false positive.

Only move bare type annotations to consumed_uncertain when the
uncertainty comes from except/try blocks, not from if/elif tests.
If/elif branches are guaranteed to execute one path, so a bare
annotation preceding them should not trigger possibly-used-before-assignment.

Adds regression test for the pandas pattern where a bare annotation
precedes an if/elif chain covering all runtime values.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@worksbyfriday
Copy link
Copy Markdown
Contributor Author

Good catch — fixed.

The bare annotation filter was too broad: it moved bare annotations to consumed_uncertain whenever any uncertain definitions existed, including those from if/elif tests. But if/elif branches are guaranteed to execute one path, so the bare annotation masking was correct there.

The fix tracks whether uncertainty came from except/try blocks specifically (where execution may genuinely not happen) vs if/elif tests (where one branch will always execute). Bare annotations are only demoted when the uncertainty source is except/try.

Added a regression test for the pandas pattern (bare_annotation_with_if_elif). All 27 used-before-assignment tests pass.

Inline the has_except_uncertainty flag update into single
expressions to stay under the 50-statement limit. Remove
redundant per-block comments since the tracking purpose is
documented in the block header.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 1, 2026

🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉

This comment was generated for commit 9391b02

@jacobtylerwalls
Copy link
Copy Markdown
Member

I'll review this in the next week or so.

Comment on lines -633 to -634
# If this node is in a Finally block of a Try/Finally,
# filter out assignments in the try portion, assuming they may fail
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please revert the unnecessary comment deletions like this (there are more than one).

uncertain_nodes = self._uncertain_nodes_in_except_blocks(
found_nodes, node, node_statement
)
has_except_uncertainty = has_except_uncertainty or bool(uncertain_nodes)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this project's style prefers implicit booleanness.

print(err)


def bare_annotation_with_if_elif(axis):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add a combination test case that involves both if/elif and try/except?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C: used-before-assignment Issues related to 'used-before-assignment' check False Negative 🦋 No message is emitted but something is wrong with the code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

possibly-used-before-assignment false negative when variable has type annotation

3 participants