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

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
@codspeed

codspeed Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 3 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing nikonikolov:fix-nested-compare-left-parens (0fd0b5f) with main (94b7090)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports. ↩

@DanielNoord
DanielNoord requested a balanced review from Copilot August 26, 2026 20:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes as_string() so nested comparisons keep necessary parentheses and don’t accidentally turn into Python chained comparisons when unparsing.

Changes:

  • Update AsStringVisitor._should_wrap to always parenthesize comparison operands at equal precedence when the parent is a Compare.
  • Add regression tests for nested comparisons used as the left operand of another comparison.
  • Document the bugfix in the changelog fragment for #3239.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
astroid/nodes/as_string.py Adjusts wrapping rules so nested Compare expressions are parenthesized correctly.
tests/test_nodes.py Adds a regression test ensuring exact-text round-trips for nested comparisons and a control for real chained comparisons.
tests/testdata/python3/data/operator_precedence.py Adds precedence/round-trip fixtures covering nested comparisons.
doc/whatsnew/fragments/3239.bugfix Documents the behavioral change and references the issue.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +77 to +85
if node_precedence == child_precedence:
if isinstance(node, nodes.Compare):
# Comparisons chain rather than associate, so a comparison
# operand always needs parentheses: (a < b) == (c < d)
return True
if is_left != node.op_left_associative():
# 3 - (4 - 5)
# (2**3)**4
return True
@codecov

codecov Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.65%. Comparing base (94b7090) to head (0fd0b5f).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #3239   +/-   ##
=======================================
  Coverage   93.65%   93.65%           
=======================================
  Files          93       93           
  Lines       11613    11616    +3     
=======================================
+ Hits        10876    10879    +3     
  Misses        737      737           
Flag Coverage Δ
linux 93.51% <100.00%> (+<0.01%) ⬆️
pypy 93.65% <100.00%> (+<0.01%) ⬆️
windows 93.62% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
astroid/nodes/as_string.py 97.36% <100.00%> (+0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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.

2 participants