Skip to content

Fix as_string() dropping parentheses around a comparison that is the left operand of another comparison - #3239

Open
nikonikolov wants to merge 1 commit into
pylint-dev:mainfrom
nikonikolov:fix-nested-compare-left-parens
Open

Fix as_string() dropping parentheses around a comparison that is the left operand of another comparison#3239
nikonikolov wants to merge 1 commit into
pylint-dev:mainfrom
nikonikolov:fix-nested-compare-left-parens

Conversation

@nikonikolov

Copy link
Copy Markdown

Type of Changes

Type
🐛 Bug fix

Description

as_string() dropped the parentheses around a comparison used as the left
operand of another comparison:

source   : assert (a is None) == (b is None)
unparsed : assert a is None == (b is None)

The unparsed line is a chained comparison — (a is None) and (None == (b is None))
— a semantically different expression.

AsStringVisitor._should_wrap handled equal precedence purely via associativity, and
Compare used the default "left associative", so a left Compare operand was never
wrapped. But comparisons chain rather than associate, so a Compare operand of a
Compare must be parenthesized on either side (the right side was only wrapped
because is_left=False happened to disagree with the assumed left-associativity).

At equal precedence, a Compare operand of a Compare is now always wrapped.
Genuine chained comparisons (a < b < c) are unaffected: their operands sit below
Compare precedence and take the existing paths.

Regression tests: exact-text round-trips in AsStringTest.test_nested_compare_as_left_operand,
plus new lines in tests/testdata/python3/data/operator_precedence.py (both fail without
the fix).

🤖 Generated with Claude Code

…operand of another comparison

Comparisons chain rather than associate, but AsStringVisitor._should_wrap
treated Compare like a left-associative operator, so the left operand was
never parenthesized: '(a is None) == (b is None)' round-tripped to
'a is None == (b is None)', which Python parses as the semantically
different chained comparison '(a is None) and (None == (b is None))'.
Comparators on the right only kept their parentheses because is_left=False
happened to disagree with the assumed left-associativity.

At equal precedence, a Compare operand of a Compare is now always wrapped,
on either side. Genuine chained comparisons are unaffected: their operands
sit below Compare precedence and take the existing paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nikonikolov
nikonikolov force-pushed the fix-nested-compare-left-parens branch from eb85057 to 0fd0b5f Compare August 19, 2026 20:26
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.

1 participant