fixhunt

ruff PLR1716

labelled unknown
Runs / 2026-10-17T21-01-44-366Z-x70i-ruff-PLR1716ruff 0.17.12026-10-17 21:01 UTC8 programs × 2 round(s)16 programs, 2 fired, 2 fixed, 0 findings, 3 rejectedmodel cost $0.0034

Analysis Nemotron 3 Super

Flags chains of two binary comparisons connected by 'and' where the middle operand is identical, suggesting simplification to a chained comparison.

Fix: Rewrites 'left op middle and middle op right' into 'left op middle op right', preserving the operators and operands exactly as they appear.

10 hazards
  • If the middle operand has side effects (e.g., a function call), evaluating it once in the chained form vs twice in the original changes behavior.
  • If the middle operand is an overloaded property with __get__ side effects, repeated access differs from single access.
  • In chained comparisons, Python evaluates the middle operand only once; if the original relied on two evaluations (e.g., for mutation detection), semantics break.
  • If the middle operand is a mutable expression accessed via indexing or attribute, and the first comparison mutates it, the second comparison sees the mutated value—chaining prevents this intermediate state.
  • If the operators are not identical (e.g., '<' and '>'), the rule does not fire, but misidentification could occur if shape matching is flawed.
  • If the expressions are parenthesized for grouping (e.g., '(x < y) and (y < z)'), the fix may incorrectly strip parentheses needed for precedence in larger expressions.
  • If the chain involves non-transitive operators (e.g., 'is' with '=='), the chained form may not reflect the original logic due to differing associativity.
  • If the middle operand is a lambda or comprehension whose evaluation order affects scoping or side effects, reducing evaluations alters behavior.
  • If comments are attached to the 'and' token or middle operand, the rewrite may discard them during AST reformatting.
  • If the code relies on short-circuiting of 'and' for error suppression (e.g., 'a and b.c where b might be None'), chaining removes this short-circuit, potentially raising AttributeError.

Programs

idresulttitlehazard
r1-p001silentNon-identical operatorsIf the operators are not identical (e.g., '<' and '>'), the rule does not fire, but misidentification could occur if shape matching is flawed.
r1-p002fix okStar-imports and module cleanupstar-imports, module-level side effects, __all__
r1-p003invalidComments on 'and' or middle operandIf comments are attached to the 'and' token or middle operand, the rewrite may discard them during AST reformatting.
r1-p004silent prediction offOverloaded property with __get__ side effectsIf the middle operand is an overloaded property with __get__ side effects, repeated access differs from single access.
r1-p005silent prediction offMiddle operand with side effects (function call)If the middle operand has side effects (e.g., a function call), evaluating it once in the chained form vs twice in the original changes behavior.
r1-p006invalidBytes vs str encodingbytes vs str, encoding and newline handling, text vs binary modes
r1-p007silent prediction offTruthiness and is/== distinctionstruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r1-p008silent prediction offContext managers without close()context managers that are not files, objects with write() but no close()
r2-p001silent prediction offOverloaded Property with __get__ Side EffectsRepeated access to a property with __get__ side effects changes state between evaluations.
r2-p002silent prediction offpathlib.Path Relative Path BehaviorPath object equality depends on resolution state; accessing .resolve() twice yields different results than once due to internal caching or symlink handling.
r2-p003fix okParenthesized Grouping Affecting PrecedenceStripping parentheses around subexpressions in a chain alters meaning when embedded in larger boolean expressions due to operator precedence.
r2-p004silent prediction offClass vs Instance Attributes with Descriptor Side EffectsAccessing a descriptor via instance triggers __get__; repeated access in chain calls it multiple times, altering state.
r2-p005silent prediction offDict Key Lookup with Side Effects in __missing__Evaluating the same dict key lookup twice in a chain triggers __missing__ twice, causing double side effects.
r2-p006silent prediction offEvaluation Order and Side Effects in Function CallsMerged evaluation changes order of side effects; original: left then middle then right; fixed: left, middle, right but middle only once — alters sequence.
r2-p007invalidBytes vs Str Encoding in ComparisonComparing bytes and str raises TypeError in Python 3; repeated access doesn't change type, but the rule may fire on invalid code that crashes differently before/after fix.
r2-p008silent prediction offStar-Imports and Module-Level Side EffectsStar-import executes module top-level code; if the module has side effects, importing it multiple times (as in repeated attribute access) vs once changes behavior.

Sources

r1-p001 · Non-identical operators
1def main():
2 x, y, z = 1, 2, 1
3 result = x < y and y > z # Not same operator; should NOT trigger
4 print(f'{result}')
5main()
True
r1-p002 · Star-imports and module cleanup
1__all__ = ['helper']
2def helper(x):
3 return x * 2
4from helper_mod import * # Simulate via exec
5exec('''
6def helper(x):
7 return x + 1
8''', globals(), locals())
9a, b, c = 1, 2, 3
10if a < b and b < c:
11 print(helper(10))
12# Cleanup
13if 'helper_mod' in locals():
14 del helper_mod
--- orig.py	2026-10-17 17:01:54.892871770 -0400
+++ fixed.py	2026-10-17 17:01:55.281876437 -0400
@@ -7,7 +7,7 @@
     return x + 1
 ''', globals(), locals())
 a, b, c = 1, 2, 3
-if a < b and b < c:
+if a < b < c:
     print(helper(10))
 # Cleanup
 if 'helper_mod' in locals():
r1-p003 · Comments on 'and' or middle operand (generated program does not parse: Traceback (most recent call last):)
1def foo():
2 return 5
3x = foo()
4y = 10
5z = 15
6if x < y # comment on and
7 and y < z: # comment after and
8 print('in range')
9else:
10 print('out of range')
r1-p004 · Overloaded property with __get__ side effects
1class Counter:
2 def __init__(self):
3 self.count = 0
4 @property
5 def value(self):
6 self.count += 1
7 return self.count
8c = Counter()
9if c.value < 3 and c.value < 5:
10 print(f'count={c.count}')
11else:
12 print(f'count={c.count}')
count=2
r1-p005 · Middle operand with side effects (function call)
1call_count = 0
2def get_val():
3 global call_count
4 call_count += 1
5 return call_count
6if get_val() < 3 and get_val() < 5:
7 print(f'calls={call_count}')
8else:
9 print(f'calls={call_count}')
calls=2
r1-p006 · Bytes vs str encoding (generated program does not parse: Traceback (most recent call last):)
1b = b'ab'
2c = b'bc'
3d = b'cd'
4if b < c and c < d:
5 print(b.decode())
6else:
7 print(b.decode()[::-1])
8with open('test.txt', 'wb') as f:
9 f.write(b'
10')
11with open('test.txt', 'rb') as f:
12 data = f.read()
13print(repr(data))
14import os
15os.remove('test.txt')
r1-p007 · Truthiness and is/== distinctions
1class Obj:
2 def __len__(self):
3 return 0
4 def __eq__(self, other):
5 return False
6a = Obj()
7b = []
8c = {}
9if a is b and b is c:
10 print('identity')
11elif a == b and b == c:
12 print('equality')
13else:
14 print(f'a={bool(a)}, b={bool(b)}, c={bool(c)}')
a=False, b=False, c=False
r1-p008 · Context managers without close()
1class Writer:
2 def __enter__(self):
3 self.data = []
4 return self
5 def __exit__(self, *exc):
6 pass
7 def write(self, s):
8 self.data.append(s)
9with Writer() as w1, Writer() as w2:
10 if 1 < 2 and 2 < 3:
11 w1.write('hello')
12 w2.write('world')
13print(''.join(w1.data), ''.join(w2.data))
hello world
r2-p001 · Overloaded Property with __get__ Side Effects
1class Counter:
2 def __init__(self):
3 self.count = 0
4 @property
5 def value(self):
6 self.count += 1
7 return self.count
8c = Counter()
9if c.value < 2 and c.value < 3:
10 print(c.count)
11else:
12 print(0)
2
r2-p002 · pathlib.Path Relative Path Behavior
1from pathlib import Path
2import os
3os.symlink('target', 'link')
4p = Path('link')
5try:
6 if p.exists() and p.exists():
7 print('both true')
8 else:
9 print('at least one false')
10except Exception as e:
11 print(type(e).__name__, e)
12finally:
13 try:
14 os.unlink('link')
15 except:
16 pass
at least one false
r2-p003 · Parenthesized Grouping Affecting Precedence
1x, y, z = 0, 5, 10
2result = not ((x < y) and (y < z)) and True
3print(result)
4# Equivalent unfixed: not (x < y and y < z) and True -> evaluates as (not (x<y and y<z)) and True
5# After bad fix: not x < y and y < z and True -> parsed as (not x < y) and (y < z) and True
6# which is different due to 'not' binding tighter than 'and'
--- orig.py	2026-10-17 17:02:03.218971507 -0400
+++ fixed.py	2026-10-17 17:02:03.397973647 -0400
@@ -1,5 +1,5 @@
 x, y, z = 0, 5, 10
-result = not ((x < y) and (y < z)) and True
+result = not ((x < y < z)) and True
 print(result)
 # Equivalent unfixed: not (x < y and y < z) and True -> evaluates as (not (x<y and y<z)) and True
 # After bad fix: not x < y and y < z and True -> parsed as (not x < y) and (y < z) and True
False
r2-p004 · Class vs Instance Attributes with Descriptor Side Effects
1class Desc:
2 def __init__(self, val):
3 self.val = val
4 def __get__(self, obj, objtype):
5 self.val += 1
6 return self.val
7class A:
8 x = Desc(0)
9a = A()
10if a.x < 2 and a.x < 3:
11 print(a.x.val)
12else:
13 print(0)
r2-p005 · Dict Key Lookup with Side Effects in __missing__
1class SideEffectDict(dict):
2 def __missing__(self, key):
3 self[key] = key.upper()
4 return self[key]
5d = SideEffectDict()
6if d['a'] == 'A' and d['a'] == 'A':
7 print('both equal')
8else:
9 print('not both equal')
10print(d['a']) # Should be 'A' after first access, but __missing__ ran twice
both equal
A
r2-p006 · Evaluation Order and Side Effects in Function Calls
1state = []
2def f(x):
3 state.append(x)
4 return x
5x, y, z = 1, 2, 3
6if f(x) < f(y) and f(y) < f(z):
7 print(state)
8else:
9 print([0])
[1, 2, 2, 3]
r2-p007 · Bytes vs Str Encoding in Comparison (generated program does not parse: Traceback (most recent call last):)
1try:
2 if b'a' < 'b' and 'b' < b'c':
3 print('unreachable')
4 except TypeError as e:
5 print('TypeError:', e)
6except Exception as e:
7 print(type(e).__name__, e)
r2-p008 · Star-Imports and Module-Level Side Effects
1import sys
2sys.path.insert(0, '.')
3with open('mod.py', 'w') as f:
4 f.write('''side = []
5def register():
6 side.append(1)
7register()
8''')
9try:
10 from mod import *
11 if side < [2] and side < [3]: # side is list, comparing to lists
12 print(side)
13 else:
14 print([0])
15except Exception as e:
16 print(type(e).__name__, e)
17finally:
18 try:
19 import os
20 os.remove('mod.py')
21 except:
22 pass
[1]