ruff SIM223
labelled unknown
Analysis Nemotron 3 Super
Flags 'and' expressions containing at least one constant falsey literal (like [], {}, '', set(), etc.) where replacing the whole expression with either False (in boolean contexts) or truncating after the last truthy operand before the first falsey improves clarity.
Fix: Replaces the entire 'and' expression with False when used in a boolean context (e.g., directly as an if/test condition), otherwise truncates the expression to include only operands up to (but not including) the first falsey literal, preserving the original evaluation order and short-circuit behavior for the remaining truthy prefix.
9 hazards
- Rewriting 'x and []' to 'x and []' (no change) when the falsey literal is not at a truncation point does not alter behavior but may miss optimization opportunities; however, the rule only triggers when a falsey literal is present and replacement is valid.
- In 'if x and [] and y:', replacing with 'if False:' changes behavior if 'x' has side effects (e.g., function call) because the original evaluates 'x' before short-circuiting, while the fix does not evaluate 'x' at all.
- In 'a = x and [] and y', replacing with 'a = x and []' preserves short-circuiting: if 'x' is falsey, 'x' is evaluated and assigned; if 'x' is truthy, '[]' is evaluated and assigned (due to truncation after 'x'), matching the original which would assign '[]' after evaluating 'x' and then short-circuit on '[]'.
- If the expression appears in a context where its type matters (e.g., passed to a function expecting a list), replacing 'x and []' with 'x and []' (no change) is safe, but replacing with 'False' in a non-boolean context would cause a type error; however, the fix only uses 'False' in boolean contexts, so this hazard is mitigated by context detection.
- The fix does not remove or move comments attached to the operands; if a comment is placed after a truncated operand (e.g., 'x and [] # comment'), the comment may be lost if the rewrite deletes the falsey literal and surrounding whitespace without preserving trailing comments.
- In nested expressions like 'f(x and [] and y)', replacing the inner 'and' with 'x and []' alters the argument passed to f: originally, if x is truthy, f([]) is called; after fix, if x is truthy, f([]) is still called — equivalent. If x is falsey, f(x) is called in both cases. So no hazard here — but this illustrates that pure functional contexts are safe.
- If the falsey literal is a mutable default or shared object (e.g., 'x and [] and y' where [] is a literal), replacing with 'x and []' still creates a new empty list each time — no identity issue since list literals are distinct objects.
- In 'x and [] and y', if 'x' is an expression with side effects (e.g., 'print(1)'), the original evaluates 'x', then '[]', then short-circuits; the fix 'x and []' evaluates 'x', then '[]', then assigns — same side effects. No change in evaluation order or side effects for the kept prefix.
- When the 'and' chain starts with a falsey literal (e.g., '[] and x'), the fix truncates to nothing? Actually, the rule replaces with the first falsey value in non-boolean contexts — but documentation shows 'a = x and [] and y' -> 'a = x and []', implying it keeps up to the falsey. For '[] and x', the first falsey is at start, so it should become just '[]'? But the fix description says 'truncates to include only operands up to (but not including) the first falsey' — which would be empty. However, in practice, the rule likely keeps the first falsey as the result when it's the first element. Clarification needed: the silent shape '0 and y' suggests that non-literal falsey values are not flagged, so only literal falseys trigger. For '[] and x', the fix would replace with '[]' (the first falsey literal). This is equivalent: original short-circuits on first operand [], result is []; fix is []. Same.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Custom bool and len interaction | Testing interaction between __bool__ and __len__ where replacing 'x and []' with 'x' changes behavior when x is falsey due to __len__ returning 0 but __bool__ returning True. |
| r1-p002 | fired, no fix | Literal falsey at start of and-chain | Verifying that when the and-chain starts with a literal falsey like [], the fix replaces the whole expression with [] (preserving short-circuit), and that this is equivalent to original. |
| r1-p003 | fired, no fix | Truncation preserves assignment semantics | In 'a = x and [] and y', ensuring that truncation to 'x and []' preserves short-circuit behavior: if x is falsey, a gets x; if x is truthy, a gets [] (from the falsey literal), matching original. |
| r1-p004 | silent prediction off | Keyword-only and positional-only parameters | Ensuring that when the rewritten 'and' expression appears in a function call with keyword-only and positional-only parameters, the argument evaluation order and value are preserved. |
| r1-p005 | silent prediction off | Mutable default argument aliasing | Testing that when a mutable default argument is involved in an 'and' expression that gets rewritten, aliasing behavior is preserved. |
| r1-p006 | fired, no fix | Nested function call equivalence | Confirming that in a pure functional context like f(x and [] and y), the rewrite to f(x and []) does not change the argument passed, even when x is truthy or falsey. |
| r1-p007 | fired, no fix | Pathlib path handling | Ensuring that when pathlib.Path objects are used in 'and' expressions involving literal falseys, the rewrite does not alter path resolution or existence checks. |
| r1-p008 | invalid | Exception handling in and-expression | Testing that if the left operand of an 'and' expression raises an exception, the rewrite does not change exception propagation, especially when the expression is inside a try-finally block. |
| r2-p001 | fired, no fix | Side effect in left operand of boolean-context and-expression | In 'if x and [] and y:', replacing with 'if False:' skips evaluation of 'x', losing side effects. |
| r2-p002 | fired, no fix | Literal falsey at start of and-expression in assignment | For '[] and x', the fix replaces with '[]'; behavior matches original short-circuit, but we verify equivalence under mutation. |
| r2-p003 | fired, no fix | Side effects in kept prefix of and-expression | In 'x and [] and y', fix becomes 'x and []'; side effects in 'x' occur in both versions. |
| r2-p004 | fired, no fix | And-expression in function argument: preserved behavior | Inner 'and' rewritten inside f(); if x truthy, both versions pass []; if falsey, both pass x. |
| r2-p005 | fired, no fix | Type-sensitive context: and-expression in list construction | Fix avoids False in non-boolean context; 'x and []' remains as-is, preserving type expectations. |
| r2-p006 | fired, no fix | Comment preservation around modified and-expression | Comments near the and-expression should not interfere; we verify logic unchanged despite comment proximity. |
| r2-p007 | fired, no fix | File write: and-expression in mode or data | Using and-expression in file open mode or write data; fix must not alter behavior. |
| r2-p008 | fired, no fix | Evaluation order: and-expression with function calls and mutation | Left-to-right evaluation and short-circuiting must be preserved after truncation. |
Sources
r1-p001 · Custom bool and len interaction
1class X:
2 def __len__(self): return 0
3 def __bool__(self): return True
4x = X()
5result = x and []
6print('result:', result)
7print('bool(x):', bool(x))
8print('len(x):', len(x))
result: [] bool(x): True len(x): 0
r1-p002 · Literal falsey at start of and-chain
1lst = [1, 2, 3]
2result = [] and lst
3print('result:', result)
4# After fix: result = []
5# Original: evaluates [], returns [] (falsey), short-circuits, lst not touched
6print('lst unchanged:', lst)
result: [] lst unchanged: [1, 2, 3]
r1-p003 · Truncation preserves assignment semantics
1def get_x(b): return 0 if b else 5
2x = get_x(True) # truthy
3y = 'hello'
4a = x and [] and y
5print('a:', a)
6# If x is truthy, original evaluates x (truthy), then [] (falsey) -> assigns []
7# Fix: x and [] -> same
8# Now make x falsey
9x = get_x(False)
10a = x and [] and y
11print('a when x falsey:', a)
12# Original: x is falsey -> assigns x (0)
13# Fix: x and [] -> evaluates x (falsey) -> assigns x (0)
14# Same result
a: 0 a when x falsey: []
r1-p004 · Keyword-only and positional-only parameters
1def f(pos, /, *, kw):
2 print(f'pos={pos}, kw={kw}')
3x = []
4# Rule triggers on: x and []
5f(x and [], kw=42)
6# Original: x is [] (falsey) -> returns []
7# Fix: truncates to nothing? But per hazard 2, for [] and x it becomes []; here x and [] -> if x falsey, result is x ([])
8# So f([], kw=42) in both cases
9# Now make x truthy
10x = [1]
11f(x and [], kw=42)
12# Original: x truthy -> evaluate [] -> returns []
13# Fix: x and [] -> x truthy -> evaluate [] -> returns []
14# Same
pos=[], kw=42 pos=[], kw=42
r1-p005 · Mutable default argument aliasing
1def f(a=[]):
2 a.append(1)
3 return a
4x = f() # [] -> [1]
5y = f() # [1] -> [1,1]
6# Now use in and-expression
7z = x and []
8print('z:', z)
9# Original: x is [1,1] (truthy) -> evaluate [] -> returns []
10# Fix: x and [] -> same
11# Now make x falsey by clearing
12x.clear()
13z = x and []
14print('z after x.clear():', z)
15# Original: x is [] (falsey) -> returns x ([])
16# Fix: x and [] -> evaluates x (falsey) -> returns x ([])
17# Both times z is []; but note: in second case, z is the same list as x (aliased)
18print('z is x:', z is x)
z: [] z after x.clear(): [] z is x: True
r1-p006 · Nested function call equivalence
1def f(val):
2 return val if val is not None else 'none'
3x = []
4res = f(x and [] and 'y')
5print('res:', res)
6# x is [] -> falsey -> returns x ([]) -> f([]) -> '[]'
7# Fix: f(x and []) -> x and [] -> [] -> f([]) -> same
8x = [1]
9res = f(x and [] and 'y')
10print('res:', res)
11# x truthy -> evaluate [] -> falsey -> returns [] -> f([])
12# Fix: x and [] -> same
13# No difference
res: [] res: []
r1-p007 · Pathlib path handling
1from pathlib import Path
2p = Path('.')
3# Create a temporary file
4tmp = Path('tmp.txt')
5tmp.write_text('test')
6try:
7 # Rule triggers on: p and []
8 result = p and []
9 print('result:', result)
10 # Original: p is truthy (Path('.') exists) -> evaluate [] -> returns []
11 # Fix: p and [] -> same
12 # Now make p falsey? Path objects are truthy even if empty? Actually, Path is always truthy.
13 # Instead, use a falsey literal at start
14 result2 = [] and p
15 print('result2:', result2)
16 # Original: [] -> falsey -> returns []
17 # Fix: becomes []
18 # Same
19finally:
20 if tmp.exists():
21 tmp.unlink()
result: [] result2: []
r1-p008 · Exception handling in and-expression (generated program does not parse: Traceback (most recent call last):)
1def bad():\n raise ValueError('bad')\ntry:\n # Rule triggers on: bad() and []\n result = bad() and []\nexcept ValueError as e:\n print('Caught:', type(e).__name__, str(e))\n# Original: calls bad() -> raises -> exception propagates\n# Fix: truncates to nothing? But per rule, in non-boolean context, replaces with first falsey? No: bad() and [] -> if bad() raises, we never get to []\n# The fix for 'x and []' in non-boolean context: if x is not a literal falsey, but the rule only fires if there's a literal falsey. Here, [] is literal falsey, so rule fires.
2# Fix: replaces 'bad() and []' with 'bad()'? Or truncates to before first falsey: bad() is first, then [] is falsey -> so keep up to but not including [] -> just 'bad()'\n# So fix would be: result = bad()\n# Same behavior: calls bad() -> raises\n# So exception behavior identical.
r2-p001 · Side effect in left operand of boolean-context and-expression
1def f():
2 print('side')
3 return True
4x = f()
5y = 2
6if x and [] and y:
7 print('taken')
8else:
9 print('skipped', x)
side skipped True
r2-p002 · Literal falsey at start of and-expression in assignment
1lst = [1]
2x = lst and [] and lst
3print('result:', x)
4lst.append(2)
5print('lst after:', lst)
result: [] lst after: [1, 2]
r2-p003 · Side effects in kept prefix of and-expression
1def g():
2 print('executed')
3 return 1
4x = g()
5y = never_defined if False else 3
6val = x and [] and y
7print('val:', val)
executed val: []
r2-p004 · And-expression in function argument: preserved behavior
1def f(arg):
2 print('got:', arg)
3x = 1
4f(x and [] and 2)
5x = 0
6f(x and [] and 2)
got: [] got: 0
r2-p005 · Type-sensitive context: and-expression in list construction
1x = 1
2lst = [x and [] and [9]]
3print('list contents:', lst)
4print('type of element:', type(lst[0]))
list contents: [[]] type of element: <class 'list'>
r2-p006 · Comment preservation around modified and-expression
1x = 1 # input
2# intermediate: x and []
3y = x and [] and 5 # compute
4print('y:', y)
y: []
r2-p007 · File write: and-expression in mode or data
1x = 1
2mode = 'w' and [] and 'w'
3data = x and [] and 'hello'
4with open('test.txt', mode) as f:
5 f.write(data)
6with open('test.txt', 'r') as f:
7 print('read:', repr(f.read()))
8import os
9os.remove('test.txt')
r2-p008 · Evaluation order: and-expression with function calls and mutation
1state = []
2def log():
3 state.append('log')
4 return True
5x = log()
6res = x and [] and log()
7print('state:', state)
8print('res:', res)
state: ['log'] res: []