ruff RUF021
labelled unknown
Analysis Nemotron 3 Super
Flags chains of 'and'/'or' without explicit precedence via parentheses.
Fix: Inserts parentheses around subexpressions to enforce 'and' before 'or' precedence, e.g., rewriting 'a or b and c' as 'a or (b and c)' and 'd and e or f' as '(d and e) or f'.
11 hazards
- Rewriting 'a or b and c' as 'a or (b and c)' changes evaluation if 'a' has side effects and is truthy, as 'b and c' would no longer be evaluated.
- Rewriting 'd and e or f' as '(d and e) or f' changes evaluation if 'd' is falsey and 'e' has side effects, as 'e' would now be evaluated in the original but not in the fix due to short-circuiting.
- If 'b' or 'e' is a property with a getter that raises an exception, moving its evaluation inside parentheses may cause the exception to be raised in different contexts.
- In lambdas, adding parentheses may alter binding precedence if the lambda body is part of a larger expression, though rare.
- Comments placed between operators (e.g., 'a # comment\n or b # comment\n and c') may be lost or misplaced during rewrite if the tool does not preserve comment attachment.
- If the expression is part of a tuple or list unpacking, adding parentheses could be interpreted as a tuple literal, altering structure (e.g., 'x, y = a or b and c' vs 'x, y = a or (b and c)' — same here, but risky in nested contexts).
- In default argument values, rewriting could affect when side effects occur due to early binding, though the fix doesn't change timing relative to function definition.
- If 'a', 'b', or 'c' are overloaded operators (e.g., __or__, __and__) in a custom class, adding parentheses does not change semantics as these are still method calls, but evaluation order of operands is preserved — low risk, but noted for completeness.
- In async contexts, if 'b' or 'e' is an awaitable, rewriting does not change await behavior as the fix doesn't introduce or remove awaits — but if mistakenly wrapped in a non-async context, could cause issues; however, the fix preserves expression structure so this is unlikely.
- If the code uses a linter or formatter that expects specific spacing around operators, the autofix may produce formatting that conflicts with project style, requiring reformat.
- In string annotations or forward references (rare for expressions), parentheses could be misinterpreted, though this applies only to annotations, not executable code.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Short-circuit side effect | Rewriting 'd and e or f' as '(d and e) or f' changes evaluation if 'd' is falsey and 'e' has side effects, as 'e' would now be evaluated in the original but not in the fix due to short-circuiting. |
| r1-p002 | invalid | Walrus in condition | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p003 | fix ok | Lambda binding precedence | In lambdas, adding parentheses may alter binding precedence if the lambda body is part of a larger expression, though rare. |
| r1-p004 | fix ok | Truthiness edge | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r1-p005 | fix ok | Bytes vs str | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p006 | invalid | Context manager write-only | context managers that are not files, objects with write() but no close() |
| r1-p007 | fix ok | Pathlib vs os.path | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r1-p008 | fix ok | Default arg side effect | In default argument values, rewriting could affect when side effects occur due to early binding, though the fix doesn't change timing relative to function definition. |
| r2-p001 | fix ok | Side effects in chained boolean - evaluation order changed | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p002 | fix ok | Async function call inside boolean chain - await safety check | In async contexts, if 'b' or 'e' is an awaitable, rewriting does not change await behavior as the fix doesn't introduce or remove awaits — but if mistakenly wrapped in a non-async context, could cause |
| r2-p003 | fix ok | Comments stripped during boolean rewrite causing confusion | Comments placed between operators (e.g., 'a # comment\n or b # comment\n and c') may be lost or misplaced during rewrite if the tool does not preserve comment attachment. |
| r2-p004 | fix ok | Attached docstring/comment lost during AST transformation | comments and docstrings attached to the removed or moved code |
| r2-p005 | fix ok | Truthy left operand skips right evaluation after fix | Rewriting 'a or b and c' as 'a or (b and c)' changes evaluation if 'a' has side effects and is truthy, as 'b and c' would no longer be evaluated. |
| r2-p006 | fix ok | Falsey left operand causes right evaluation in original but not after fix | Rewriting 'd and e or f' as '(d and e) or f' changes evaluation if 'd' is falsey and 'e' has side effects, as 'e' would now be evaluated in the original but not in the fix due to short-circuiting. |
| r2-p007 | invalid | Property descriptor side effects altered by precedence change | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p008 | fix ok | Iterator consumed prematurely due to changed evaluation order | generators vs lists, iterators consumed once, lazy vs eager evaluation |
Sources
r1-p001 · Short-circuit side effect
1def f():
2 print('f')
3 return False
4side = []
5def g():
6 side.append(1)
7 return True
8x = f() and g() or True
9print(len(side))
10x = (f() and g()) or True
11print(len(side))
--- orig.py 2026-10-17 17:42:22.656209768 -0400 +++ fixed.py 2026-10-17 17:42:22.825208522 -0400 @@ -5,7 +5,7 @@ def g(): side.append(1) return True -x = f() and g() or True +x = (f() and g()) or True print(len(side)) x = (f() and g()) or True print(len(side))
f 0 f 0
r1-p002 · Walrus in condition (generated program does not parse: Traceback (most recent call last):)
1ls = [0]
2if (tmp := ls[0]) == 0 or tmp := ls.pop():
3 pass
4else:
5 tmp = -1
6print(tmp)
7if ((tmp := ls[0]) == 0) or tmp := ls.pop():
8 pass
9else:
10 tmp = -1
11print(tmp)
r1-p003 · Lambda binding precedence
1f = lambda: True and False or True
2result = f() and print('inner') or print('outer')
3f = lambda: (True and False) or True
4result = f() and print('inner') or print('outer')
--- orig.py 2026-10-17 17:42:23.056206819 -0400 +++ fixed.py 2026-10-17 17:42:23.213205662 -0400 @@ -1,4 +1,4 @@ -f = lambda: True and False or True -result = f() and print('inner') or print('outer') f = lambda: (True and False) or True -result = f() and print('inner') or print('outer') +result = (f() and print('inner')) or print('outer') +f = lambda: (True and False) or True +result = (f() and print('inner')) or print('outer')
inner outer inner outer
r1-p004 · Truthiness edge
1class A:
2 def __bool__(self):
3 print('A bool')
4 return False
5 def __len__(self):
6 return 0
7a = A()
8b = []
9if a or b and True:
10 print('yes')
11else:
12 print('no')
13if a or (b and True):
14 print('yes')
15else:
16 print('no')
--- orig.py 2026-10-17 17:42:23.313204925 -0400 +++ fixed.py 2026-10-17 17:42:23.468203783 -0400 @@ -6,7 +6,7 @@ return 0 a = A() b = [] -if a or b and True: +if a or (b and True): print('yes') else: print('no')
A bool no A bool no
r1-p005 · Bytes vs str
1data = b'hello'
2empty = b''
3if data or empty and b'world':
4 res = b'yes'
5else:
6 res = b'no'
7print(res)
8if data or (empty and b'world'):
9 res = b'yes'
10else:
11 res = b'no'
12print(res)
--- orig.py 2026-10-17 17:42:23.571203023 -0400 +++ fixed.py 2026-10-17 17:42:23.733201830 -0400 @@ -1,6 +1,6 @@ data = b'hello' empty = b'' -if data or empty and b'world': +if data or (empty and b'world'): res = b'yes' else: res = b'no'
b'yes' b'yes'
r1-p006 · Context manager write-only (generated program does not parse: Traceback (most recent call last):)
1class W:
2 def write(self, s):
3 print(s, end='')
4 def __enter__(self):
5 return self
6 def __exit__(self, *e):
7 pass
8w = W()
9flag = False
10with w if flag or True:
11 w.write('x')
12with w if (flag or True):
13 w.write('y')
14print()
r1-p007 · Pathlib vs os.path
1import os
2try:
3 os.remove('test.txt')
4except FileNotFoundError:
5 pass
6p = 'test.txt'
7if not os.path.exists(p) or open(p, 'w').close() and True:
8 pass
9else:
10 print('fail')
11if (not os.path.exists(p)) or (open(p, 'w').close() and True):
12 pass
13else:
14 print('fail')
15try:
16 os.remove('test.txt')
17except:
18 pass
--- orig.py 2026-10-17 17:42:23.971200076 -0400 +++ fixed.py 2026-10-17 17:42:24.130198905 -0400 @@ -4,7 +4,7 @@ except FileNotFoundError: pass p = 'test.txt' -if not os.path.exists(p) or open(p, 'w').close() and True: +if not os.path.exists(p) or (open(p, 'w').close() and True): pass else: print('fail')
r1-p008 · Default arg side effect
1side = []
2def log():
3 side.append(1)
4 return 0
5def f(a=log() or True and False):
6 return len(side)
7print(f())
8side = []
9def g(a=(log() or True) and False):
10 return len(side)
11print(g())
--- orig.py 2026-10-17 17:42:24.221198235 -0400 +++ fixed.py 2026-10-17 17:42:24.392196976 -0400 @@ -2,7 +2,7 @@ def log(): side.append(1) return 0 -def f(a=log() or True and False): +def f(a=log() or (True and False)): return len(side) print(f()) side = []
1 1
r2-p001 · Side effects in chained boolean - evaluation order changed
1def f():
2 print('f called')
3 return False
4def g():
5 print('g called')
6 return True
7def h():
8 print('h called')
9 return True
10x = f() or g() and h()
11print(x)
--- orig.py 2026-10-17 17:42:30.131154810 -0400 +++ fixed.py 2026-10-17 17:42:30.296153600 -0400 @@ -7,5 +7,5 @@ def h(): print('h called') return True -x = f() or g() and h() +x = f() or (g() and h()) print(x)
f called g called h called True
r2-p002 · Async function call inside boolean chain - await safety check
1import asyncio
2async def get_val(name, val):
3 print(f'{name} computed')
4 return val
5async def main():
6 a = await get_val('a', False)
7 b = await get_val('b', True)
8 c = await get_val('c', True)
9 res = a or b and c
10 print(res)
11asyncio.run(main())
--- orig.py 2026-10-17 17:42:30.388152926 -0400 +++ fixed.py 2026-10-17 17:42:30.666150889 -0400 @@ -6,6 +6,6 @@ a = await get_val('a', False) b = await get_val('b', True) c = await get_val('c', True) - res = a or b and c + res = a or (b and c) print(res) asyncio.run(main())
a computed b computed c computed True
r2-p003 · Comments stripped during boolean rewrite causing confusion
1# First operand
2a = False # left side
3# Middle
4eval_val = True # middle
5# Last
6last_val = False # right
7result = a or eval_val and last_val
8print(result)
--- orig.py 2026-10-17 17:42:30.840149613 -0400 +++ fixed.py 2026-10-17 17:42:31.006148397 -0400 @@ -4,5 +4,5 @@ eval_val = True # middle # Last last_val = False # right -result = a or eval_val and last_val +result = a or (eval_val and last_val) print(result)
False
r2-p004 · Attached docstring/comment lost during AST transformation
1def compute():
2 """This function computes a boolean."""
3 flag_a = False
4 flag_b = True
5 flag_c = False
6 result = flag_a or flag_b and flag_c # Critical path logic
7 return result
8print(compute())
--- orig.py 2026-10-17 17:42:31.102147694 -0400 +++ fixed.py 2026-10-17 17:42:31.262146523 -0400 @@ -3,6 +3,6 @@ flag_a = False flag_b = True flag_c = False - result = flag_a or flag_b and flag_c # Critical path logic + result = flag_a or (flag_b and flag_c) # Critical path logic return result print(compute())
False
r2-p005 · Truthy left operand skips right evaluation after fix
1def get_a():
2 print('A evaluated')
3 return True
4def get_b():
5 print('B evaluated')
6 return False
7def get_c():
8 print('C evaluated')
9 return True
10res = get_a() or get_b() and get_c()
11print('Result:', res)
--- orig.py 2026-10-17 17:42:31.360145805 -0400 +++ fixed.py 2026-10-17 17:42:31.517144655 -0400 @@ -7,5 +7,5 @@ def get_c(): print('C evaluated') return True -res = get_a() or get_b() and get_c() +res = get_a() or (get_b() and get_c()) print('Result:', res)
A evaluated Result: True
r2-p006 · Falsey left operand causes right evaluation in original but not after fix
1def get_d():
2 print('D evaluated')
3 return False
4def get_e():
5 print('E evaluated')
6 return True
7def get_f():
8 print('F evaluated')
9 return False
10res = get_d() and get_e() or get_f()
11print('Result:', res)
--- orig.py 2026-10-17 17:42:31.612143959 -0400 +++ fixed.py 2026-10-17 17:42:31.769142810 -0400 @@ -7,5 +7,5 @@ def get_f(): print('F evaluated') return False -res = get_d() and get_e() or get_f() +res = (get_d() and get_e()) or get_f() print('Result:', res)
D evaluated F evaluated Result: False
r2-p007 · Property descriptor side effects altered by precedence change (generated program is not deterministic)
1class Flag:
2 def __init__(self, name, val):
3 self.name = name
4 self._val = val
5 def __bool__(self):
6 print(f'{self.name} accessed')
7 return self._val
8
9a = Flag('A', True)
10b = Flag('B', False)
11c = Flag('C', True)
12res = a or b and c
13print('Result:', res)
r2-p008 · Iterator consumed prematurely due to changed evaluation order
1def gen():
2 yield 1
3 yield 0
4 yield 1
5it = gen()
6def next_or_default():
7 try:
8 return next(it)
9 except StopIteration:
10 return 0
11res = next_or_default() or next_or_default() and next_or_default()
12print('Result:', res)
13try:
14 next(it)
15 print('Extra value:', next(it))
16except StopIteration:
17 print('Iterator exhausted')
--- orig.py 2026-10-17 17:42:31.949141492 -0400 +++ fixed.py 2026-10-17 17:42:32.110140313 -0400 @@ -8,7 +8,7 @@ return next(it) except StopIteration: return 0 -res = next_or_default() or next_or_default() and next_or_default() +res = next_or_default() or (next_or_default() and next_or_default()) print('Result:', res) try: next(it)
Result: 1 Extra value: 1