ruff PLR1714
labelled unknown
Analysis Nemotron 3 Super
Flags repeated equality comparisons like 'x == a or x == b' that can be rewritten using a membership test.
Fix: Rewrites repeated equality comparisons of the form 'expr == val1 or expr == val2 ...' into 'expr in {val1, val2, ...}' when all values are hashable, otherwise into 'expr in (val1, val2, ...)'.
8 hazards
- The expression on the left-hand side may have side effects (e.g., a function call), causing it to be evaluated multiple times in the original but only once in the fix.
- If any compared value is unhashable (e.g., a list or dict), the fix uses a tuple, but the original may rely on short-circuiting to avoid evaluating later values after a match.
- The fix evaluates all values in the set/tuple upfront, which can raise exceptions during construction (e.g., division by zero in a literal) that would not occur in the original if earlier comparisons succeeded.
- Comments attached to individual comparison operands may be lost or misplaced during the rewrite into a single expression.
- If the left-hand expression is mutable and changed between comparisons (e.g., via aliasing), the original may see different values while the fix uses a single evaluated value.
- In rare cases, custom __eq__ implementations may behave differently when invoked multiple times vs. once due to state changes.
- The fix may alter operator precedence interactions if the original expression was part of a larger boolean expression relying on short-circuiting for control flow.
- If the compared values include duplicates, the deduplication in a set may change behavior if equality is not idempotent or if duplicates had semantic meaning (though unlikely).
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Comments inside chained equality stripped by IN conversion | comments and docstrings attached to the removed or moved code |
| r1-p002 | fired, no fix | Inline comments on operands lost during rewrite | Comments attached to individual comparison operands may be lost or misplaced during the rewrite into a single expression. |
| r1-p004 | silent prediction off | Path comparison broken by IN rewrite assuming same type | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r1-p005 | silent prediction off | Side effects in expr duplicated by IN rewrite | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r1-p006 | silent prediction off | Duplicate values in OR chain deduplicated by set changing meaning | If the compared values include duplicates, the deduplication in a set may change behavior if equality is not idempotent or if duplicates had semantic meaning (though unlikely). |
| r1-p007 | fired, no fix | Type annotation at runtime affected by missing name after rewrite | type annotations evaluated at runtime, typing-only imports |
| r1-p008 | silent prediction off | Dict key order and duplicate keys changed by set conversion | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
| r2-p001 | silent prediction off | Generators exhausted after first iteration | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r2-p002 | fired, no fix | Keyword-only argument default evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p003 | fired, no fix | Short-circuiting in larger boolean expression | The fix may alter operator precedence interactions if the original expression was part of a larger boolean expression relying on short-circuiting for control flow. |
| r2-p004 | fired, no fix | Runtime evaluation of type annotation | type annotations evaluated at runtime, typing-only imports |
| r2-p005 | silent prediction off | Bytes vs str equality with encoding | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p006 | fired, no fix | Async function awaited twice | async def/await ordering, async generators |
| r2-p007 | fired, no fix | Property with side effect accessed twice | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p008 | silent prediction off | Pathlib vs os.path symlink resolution | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
Sources
r1-p001 · Comments inside chained equality stripped by IN conversion
1# Setup
2x = 1
3# First check: magic number one
4if x == 1 or x == 2: # Second: magic number two
5 print('matched')
6else:
7 print('no match')
matched
r1-p002 · Inline comments on operands lost during rewrite
1value = 'alpha'
2# First option: primary
3if value == 'alpha' or value == 'beta': # Second: fallback
4 print('got alpha or beta')
5else:
6 print('other')
got alpha or beta
r1-p004 · Path comparison broken by IN rewrite assuming same type
1import os
2from pathlib import Path
3p = Path('tmp')
4p.write_text('test')
5try:
6 if str(p) == 'tmp' or os.fsencode(p) == b'tmp':
7 print('path matched')
8 else:
9 print('no match')
10except Exception as e:
11 print(type(e).__name__, e)
12finally:
13 p.unlink(missing_ok=True)
path matched
r1-p005 · Side effects in expr duplicated by IN rewrite
1counter = 0
2def getx():
3 global counter
4 counter += 1
5 return counter
6if getx() == 1 or getx() == 2:
7 print('first or second call matched')
8print('total calls:', counter)
first or second call matched total calls: 1
r1-p006 · Duplicate values in OR chain deduplicated by set changing meaning
1class EqCounter:
2 def __init__(self, val):
3 self.val = val
4 self.eq_count = 0
5 def __eq__(self, other):
6 self.eq_count += 1
7 return self.val == other
8obj = EqCounter(5)
9if obj == 5 or obj == 5:
10 print('matched,', obj.eq_count, 'comparisons')
11else:
12 print('no match,', obj.eq_count, 'comparisons')
matched, 1 comparisons
r1-p007 · Type annotation at runtime affected by missing name after rewrite
1T = int
2if 1 == T or 2 == T: # T is int at runtime
3 print('matched')
4else:
5 print('no match')
6print('T is', T)
no match T is <class 'int'>
r1-p008 · Dict key order and duplicate keys changed by set conversion
1class BadHash:
2 def __init__(self, v):
3 self.v = v
4 def __hash__(self):
5 return hash(self.v)
6 def __eq__(self, other):
7 return isinstance(other, BadHash) and self.v == other.v
8d = {BadHash(1): 'a', BadHash(2): 'b'}
9k = BadHash(1)
10if k in d or k == BadHash(2):
11 print('found via dict or equality')
12else:
13 print('not found')
found via dict or equality
r2-p001 · Generators exhausted after first iteration
1def gen():
2 yield 1
3 yield 2
4
5g = gen()
6a = []
7for _ in range(2):
8 if next(g) == 1 or next(g) == 2:
9 a.append('yes')
10 else:
11 a.append('no')
12print(a)
r2-p002 · Keyword-only argument default evaluated once
1L = []
2def f(x, *, y=L.append('default') or 0):
3 return x == 1 or x == 2
4print(f(1))
5print(f(2))
6print(L)
True True ['default']
r2-p003 · Short-circuiting in larger boolean expression
1def side():
2 print('side')
3 return True
4
5if side() and (x == 1 or x == 2):
6 print('taken')
7else:
8 print('skipped')
9x = 1
side
r2-p004 · Runtime evaluation of type annotation
1T = int
2def f(x) -> 'T':
3 return x == 1 or x == 2
4print(f(1))
5print(f(2))
True True
r2-p005 · Bytes vs str equality with encoding
1b = b'hello'
2s = 'hello'
3if b == b'hello' or s == 'hello':
4 print('match')
5else:
6 print('no match')
match
r2-p006 · Async function awaited twice
1async def f():
2 return 1
3
4async def main():
5 r = await f()
6 if r == 1 or r == 2:
7 print('yes')
8 else:
9 print('no')
10import asyncio
11asyncio.run(main())
yes
r2-p007 · Property with side effect accessed twice
1class C:
2 def __init__(self):
3 self._x = 0
4 @property
5 def x(self):
6 self._x += 1
7 return self._x
8
9c = C()
10if c.x == 1 or c.x == 2:
11 print(c._x)
12else:
13 print('none')
1
r2-p008 · Pathlib vs os.path symlink resolution
1import os
2import pathlib
3os.symlink('tmp', 'link')
4try:
5 p = pathlib.Path('link')
6 o = os.path
7 if p.exists() or o.exists('link'):
8 print('exists')
9 else:
10 print('missing')
11except Exception as e:
12 print(type(e).__name__, e)
13finally:
14 try: os.unlink('link')
15 except: pass
missing