From 4fb4079c2eb76033c1be927e0d58ff5bd80d8a85 Mon Sep 17 00:00:00 2001 From: Friday Date: Tue, 17 Feb 2026 11:25:11 +0000 Subject: [PATCH 1/7] Fix used-before-assignment false negative with bare type annotations 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 #10847 --- doc/whatsnew/fragments/10847.false_negative | 4 +++ pylint/checkers/variables.py | 13 +++++++++ ...used_before_assignment_type_annotations.py | 28 +++++++++++++++++++ ...sed_before_assignment_type_annotations.txt | 1 + 4 files changed, 46 insertions(+) create mode 100644 doc/whatsnew/fragments/10847.false_negative diff --git a/doc/whatsnew/fragments/10847.false_negative b/doc/whatsnew/fragments/10847.false_negative new file mode 100644 index 0000000000..7ee0977601 --- /dev/null +++ b/doc/whatsnew/fragments/10847.false_negative @@ -0,0 +1,4 @@ +Fix ``used-before-assignment`` false negative when a variable has a bare type +annotation (without a value) and is only assigned inside ``except`` blocks. + +Closes #10847 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 5add0732c0..46938ebb38 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -651,6 +651,19 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] + # Filter out bare type annotations (AnnAssign without a value) when there + # are uncertain definitions. A bare annotation like `x: int` does not actually + # assign a value, so it should not suppress possibly-used-before-assignment + # when the real assignments are uncertain (e.g., in except blocks). + if found_nodes and self.consumed_uncertain.get(name): + found_nodes = [ + n + for n in found_nodes + if not ( + isinstance(n.parent, nodes.AnnAssign) and n.parent.value is None + ) + ] + return found_nodes def _inferred_to_define_name_raise_or_return( diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.py b/tests/functional/u/used/used_before_assignment_type_annotations.py index f4534fdad3..834094de3c 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.py +++ b/tests/functional/u/used/used_before_assignment_type_annotations.py @@ -107,3 +107,31 @@ def loop_conditional_annotated_assignment(): data={"cat": "harf"} token: str = data.get("cat") # [possibly-used-before-assignment] print(token) + + +def bare_annotation_with_except_assignment(): + """A bare type annotation should not suppress used-before-assignment + when the only real assignments are in except blocks. + + https://github.com/pylint-dev/pylint/issues/10847 + """ + result = None + err: int # pylint: disable=unused-variable + try: + result = 1 + except Exception: # pylint: disable=broad-exception-caught + err = 1 + if not result: + print(err) # [used-before-assignment] + + +def bare_annotation_with_value_and_except(): + """A type annotation with a value should suppress the warning.""" + result = None + err: int = 0 + try: + result = 1 + except Exception: # pylint: disable=broad-exception-caught + err = 1 + if not result: + print(err) diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.txt b/tests/functional/u/used/used_before_assignment_type_annotations.txt index 9b01f2fa27..c2a4608bd7 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.txt +++ b/tests/functional/u/used/used_before_assignment_type_annotations.txt @@ -3,3 +3,4 @@ used-before-assignment:28:10:28:18:value_assignment_after_access:Using variable undefined-variable:62:14:62:17:decorator_returning_incorrect_function.wrapper_with_type_and_no_value:Undefined variable 'var':HIGH possibly-used-before-assignment:97:17:97:21:conditional_annotated_assignment:Possibly using variable 'data' before assignment:CONTROL_FLOW possibly-used-before-assignment:108:17:108:21:loop_conditional_annotated_assignment:Possibly using variable 'data' before assignment:CONTROL_FLOW +used-before-assignment:125:14:125:17:bare_annotation_with_except_assignment:Using variable 'err' before assignment:CONTROL_FLOW From b4981243d36fa71e8fc0473364adcc19f5cf81ec Mon Sep 17 00:00:00 2001 From: Friday Date: Thu, 19 Feb 2026 17:17:57 +0000 Subject: [PATCH 2/7] Rework bare annotation handling: move to uncertain instead of removing 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 --- pylint/checkers/variables.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 46938ebb38..3a98cdd11d 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -651,18 +651,21 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # Filter out bare type annotations (AnnAssign without a value) when there - # are uncertain definitions. A bare annotation like `x: int` does not actually - # assign a value, so it should not suppress possibly-used-before-assignment - # when the real assignments are uncertain (e.g., in except blocks). + # Treat bare type annotations (AnnAssign without a value) as uncertain + # when there are other uncertain definitions. A bare annotation like + # `x: int` does not actually assign a value, so it should not suppress + # possibly-used-before-assignment when the real assignments are uncertain + # (e.g., in except blocks). if found_nodes and self.consumed_uncertain.get(name): - found_nodes = [ + bare_annotations = [ n for n in found_nodes - if not ( - isinstance(n.parent, nodes.AnnAssign) and n.parent.value is None - ) + if isinstance(n.parent, nodes.AnnAssign) and n.parent.value is None ] + if bare_annotations: + self.consumed_uncertain[name] += bare_annotations + bare_set = set(bare_annotations) + found_nodes = [n for n in found_nodes if n not in bare_set] return found_nodes From 95ecbab36c720999609bdde6caab316c189b6b06 Mon Sep 17 00:00:00 2001 From: Friday Date: Mon, 23 Feb 2026 21:05:51 +0000 Subject: [PATCH 3/7] Use ValueError in tests to avoid pylint: disable comments Rewrite test examples per review: use a specific exception type instead of Exception to avoid broad-exception-caught disables. --- .../u/used/used_before_assignment_type_annotations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.py b/tests/functional/u/used/used_before_assignment_type_annotations.py index 834094de3c..c58c29c243 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.py +++ b/tests/functional/u/used/used_before_assignment_type_annotations.py @@ -116,10 +116,10 @@ def bare_annotation_with_except_assignment(): https://github.com/pylint-dev/pylint/issues/10847 """ result = None - err: int # pylint: disable=unused-variable + err: int try: result = 1 - except Exception: # pylint: disable=broad-exception-caught + except ValueError: err = 1 if not result: print(err) # [used-before-assignment] @@ -131,7 +131,7 @@ def bare_annotation_with_value_and_except(): err: int = 0 try: result = 1 - except Exception: # pylint: disable=broad-exception-caught + except ValueError: err = 1 if not result: print(err) From 330ef41eabb26c3b1788d3af8d945cf8aa457acc Mon Sep 17 00:00:00 2001 From: Friday Date: Fri, 27 Feb 2026 15:59:03 +0000 Subject: [PATCH 4/7] Rewrite test examples per review feedback Use realistic int(text) conversion instead of bare assignments, and TypeError/OverflowError instead of ValueError. Co-Authored-By: Claude Opus 4.6 --- ...used_before_assignment_type_annotations.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.py b/tests/functional/u/used/used_before_assignment_type_annotations.py index c58c29c243..633cd85b93 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.py +++ b/tests/functional/u/used/used_before_assignment_type_annotations.py @@ -109,29 +109,29 @@ def loop_conditional_annotated_assignment(): print(token) -def bare_annotation_with_except_assignment(): +def bare_annotation_with_except_assignment(text): """A bare type annotation should not suppress used-before-assignment when the only real assignments are in except blocks. https://github.com/pylint-dev/pylint/issues/10847 """ - result = None err: int try: - result = 1 - except ValueError: + result = int(text) + except (TypeError, OverflowError): err = 1 - if not result: + result = -1 + if result < 0: print(err) # [used-before-assignment] -def bare_annotation_with_value_and_except(): +def bare_annotation_with_value_and_except(text): """A type annotation with a value should suppress the warning.""" - result = None err: int = 0 try: - result = 1 - except ValueError: + result = int(text) + except (TypeError, OverflowError): err = 1 - if not result: + result = -1 + if result < 0: print(err) From 02b359ab64473f72a79506fb652a832a25ecd573 Mon Sep 17 00:00:00 2001 From: Friday Date: Sun, 1 Mar 2026 14:25:04 +0000 Subject: [PATCH 5/7] Use ValueError in test examples per review feedback 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 --- .../u/used/used_before_assignment_type_annotations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.py b/tests/functional/u/used/used_before_assignment_type_annotations.py index 633cd85b93..07e0d1c8ba 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.py +++ b/tests/functional/u/used/used_before_assignment_type_annotations.py @@ -118,7 +118,7 @@ def bare_annotation_with_except_assignment(text): err: int try: result = int(text) - except (TypeError, OverflowError): + except ValueError: err = 1 result = -1 if result < 0: @@ -130,7 +130,7 @@ def bare_annotation_with_value_and_except(text): err: int = 0 try: result = int(text) - except (TypeError, OverflowError): + except ValueError: err = 1 result = -1 if result < 0: From f380c3e69cf7dfcd29ce1713abc00257821c639e Mon Sep 17 00:00:00 2001 From: Friday Date: Sun, 1 Mar 2026 15:34:37 +0000 Subject: [PATCH 6/7] Fix false positive for bare annotation with if/elif 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 --- pylint/checkers/variables.py | 20 ++++++++++++++----- ...used_before_assignment_type_annotations.py | 15 ++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 3a98cdd11d..8364f34d61 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -619,10 +619,13 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: # Filter out assignments in an Except clause that the node is not # contained in, assuming they may fail + has_except_uncertainty = False if found_nodes: uncertain_nodes = self._uncertain_nodes_in_except_blocks( found_nodes, node, node_statement ) + if uncertain_nodes: + has_except_uncertainty = True self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] @@ -635,6 +638,8 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: found_nodes, node_statement, name ) ) + if uncertain_nodes: + has_except_uncertainty = True self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] @@ -647,16 +652,21 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: found_nodes, node_statement ) ) + if uncertain_nodes: + has_except_uncertainty = True self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] # Treat bare type annotations (AnnAssign without a value) as uncertain - # when there are other uncertain definitions. A bare annotation like - # `x: int` does not actually assign a value, so it should not suppress - # possibly-used-before-assignment when the real assignments are uncertain - # (e.g., in except blocks). - if found_nodes and self.consumed_uncertain.get(name): + # when there are uncertain definitions from except/try blocks. + # A bare annotation like `x: int` does not actually assign a value, + # so it should not suppress possibly-used-before-assignment when the + # real assignments are in except blocks that may not execute. + # We only do this for except/try uncertainty, not for if/elif + # uncertainty, because if/elif branches are guaranteed to execute + # one path (the bare annotation masking is correct there). + if found_nodes and has_except_uncertainty: bare_annotations = [ n for n in found_nodes diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.py b/tests/functional/u/used/used_before_assignment_type_annotations.py index 07e0d1c8ba..a33671ee42 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.py +++ b/tests/functional/u/used/used_before_assignment_type_annotations.py @@ -135,3 +135,18 @@ def bare_annotation_with_value_and_except(text): result = -1 if result < 0: print(err) + + +def bare_annotation_with_if_elif(axis): + """A bare type annotation with if/elif should not be a false positive. + + Regression test: the pandas pattern where a bare annotation precedes + an if/elif chain that covers all runtime values. + https://github.com/pylint-dev/pylint/pull/10852#pullrequestreview-3872486590 + """ + klass: type + if axis == 0: + klass = int + elif axis == 1: + klass = str + return klass From 9391b021efc1932ccfeaf3d3958f0457fc88c748 Mon Sep 17 00:00:00 2001 From: Friday Date: Sun, 1 Mar 2026 15:53:56 +0000 Subject: [PATCH 7/7] Reduce statement count in get_next_to_consume 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 --- pylint/checkers/variables.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 8364f34d61..f359201465 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -617,43 +617,36 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # Filter out assignments in an Except clause that the node is not - # contained in, assuming they may fail + # Filter out assignments in except/try blocks, tracking whether any + # uncertainty came from these blocks (as opposed to if/elif tests). has_except_uncertainty = False if found_nodes: uncertain_nodes = self._uncertain_nodes_in_except_blocks( found_nodes, node, node_statement ) - if uncertain_nodes: - has_except_uncertainty = True + has_except_uncertainty = has_except_uncertainty or bool(uncertain_nodes) self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # If this node is in a Finally block of a Try/Finally, - # filter out assignments in the try portion, assuming they may fail if found_nodes: uncertain_nodes = ( self._uncertain_nodes_in_try_blocks_when_evaluating_finally_blocks( found_nodes, node_statement, name ) ) - if uncertain_nodes: - has_except_uncertainty = True + has_except_uncertainty = has_except_uncertainty or bool(uncertain_nodes) self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # If this node is in an ExceptHandler, - # 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( found_nodes, node_statement ) ) - if uncertain_nodes: - has_except_uncertainty = True + has_except_uncertainty = has_except_uncertainty or bool(uncertain_nodes) self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set]