ruff SIM212
labelled unknown
Analysis Nemotron 3 Super
Flags ternary conditional expressions where the condition is negated (`not x`) and suggests swapping the arms to use the positive condition.
Fix: Swaps the true and false arms of the ternary expression and removes the `not` operator from the condition, transforming `b if not a else a` into `a if a else b`.
10 hazards
- If `a` has side effects (e.g., a function call), evaluating it twice in the fixed form changes behavior compared to evaluating it once in the original.
- If `a` is an expression with mutable state, re-evaluating it may yield different results due to state changes between evaluations.
- If `a` raises an exception during evaluation, the fixed form may raise it where the original did not (or vice versa), altering control flow.
- If `a` is not idempotent (e.g., `pop()` on a list), duplicating its evaluation changes program state incorrectly.
- If `a` is a volatile expression (e.g., reading hardware or time), re-evaluation may produce inconsistent values.
- If `b` or `a` contain comments, the autofix may discard them during AST reconstruction.
- If the expression appears in a lambda or comprehension, duplicating `a` could affect scoping or closure capture unexpectedly.
- If `a` is used for its truthiness but is later mutated, the fixed form may observe a different value than intended due to re-evaluation.
- If `a` is a property with a setter, accessing it twice may trigger unintended side effects.
- If the surrounding code relies on the exact evaluation count of `a` for logging, debugging, or performance reasons, the fix alters observable behavior.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Evaluation Count Logging | If the surrounding code relies on the exact evaluation count of `a` for logging, debugging, or performance reasons, the fix alters observable behavior. |
| r1-p002 | silent prediction off | Exception Raised in Condition | Exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r1-p003 | silent prediction off | Async Generator Interaction | async def/await ordering, async generators |
| r1-p004 | silent prediction off | Custom __bool__ and Truthiness | Truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r1-p005 | silent prediction off | Comment Discard in AST Reconstruction | If `b` or `a` contain comments, the autofix may discard them during AST reconstruction. |
| r1-p006 | silent prediction off | Star Import Side Effect | star-imports, module-level side effects, __all__ |
| r1-p007 | silent prediction off | Mutable State Re-evaluation | If `a` is an expression with mutable state, re-evaluating it may yield different results due to state changes between evaluations. |
| r1-p008 | silent prediction off | File Write Truncation Hazard | file modes and truncation, partial writes, non-str data passed to write |
| r2-p001 | silent prediction off | Lambda captures changed by duplicate evaluation | If the expression appears in a lambda or comprehension, duplicating `a` could affect scoping or closure capture unexpectedly. |
| r2-p002 | silent prediction off | Shadowed built-in after redefinition | shadowed builtins and names redefined later in the module |
| r2-p003 | silent prediction off | Side effect duplicated in fix | If `a` has side effects (e.g., a function call), evaluating it twice in the fixed form changes behavior compared to evaluating it once in the original. |
| r2-p004 | silent prediction off | Keyword-only arg default re-evaluated | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p005 | silent prediction off | Exception raised in condition changes timing | If `a` raises an exception during evaluation, the fixed form may raise it where the original did not (or vice versa), altering control flow. |
| r2-p006 | silent prediction off | Property setter triggered twice | If `a` is a property with a setter, accessing it twice may trigger unintended side effects. |
| r2-p007 | silent prediction off | Iterator consumed twice | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r2-p008 | silent prediction off | Binary mode file write truncation hazard | bytes vs str, encoding and newline handling, text vs binary modes |
Sources
r1-p001 · Evaluation Count Logging
1count = 0
2class A:
3 def __bool__(self):
4 global count
5 count += 1
6 return False
7
8a = A()
9b = 'original'
10result = b if not a else a
11print(f'eval_count={count}, result={result}')
eval_count=1, result=original
r1-p002 · Exception Raised in Condition
1def bad():
2 raise ValueError('fail')
3
4try:
5 x = 'ok' if not bad() else 'no'
6except ValueError as e:
7 print(f'exception: {type(e).__name__}: {e}')
8else:
9 print(f'no exception, x={x}')
10finally:
11 print('finally ran')
exception: ValueError: fail finally ran
r1-p003 · Async Generator Interaction
1async def gen():
2 yield 1
3 await asyncio.sleep(0)
4 yield 2
5
6async def main():
7 g = gen()
8 first = await g.__anext__()
9 # Rule fires on: second if not first else 0
10 second = first if not first else 0 # not first -> False, so second=first
11 print(f'first={first}, second={second}')
12 try:
13 await g.__anext__()
14 except StopAsyncIteration:
15 print('done')
16
17import asyncio
18asyncio.run(main())
first=1, second=0
r1-p004 · Custom __bool__ and Truthiness
1class X:
2 def __bool__(self):
3 return False # always false
4
5x = X()
6y = None
7# Rule fires: z if not x else y -> since not x is True, z is chosen
8z = 'selected'
9result = z if not x else y
10print(f'x is None: {x is y}, x == y: {x == y}, bool(x): {bool(x)}')
11print(f'result={result}')
x is None: False, x == y: False, bool(x): False result=selected
r1-p005 · Comment Discard in AST Reconstruction
1# This comment should be preserved if possible
2val = 'keep' # inline comment
3flag = False
4# Rule fires on: val if not flag else 'drop'
5result = val if not flag else 'drop' # keep this comment
6print(f'result={result}')
result=keep
r1-p006 · Star Import Side Effect
1import sys
2sys.modules[__name__]._side = []
3
4def _init():
5 sys.modules[__name__]._side.append('loaded')
6
7_init()
8from __main__ import * # triggers side effect via _init
9
10# Rule fires: msg if not loaded else 'default'
11loaded = len(sys.modules[__name__]._side) > 0
12msg = 'hello'
13result = msg if not loaded else 'default'
14print(f'loaded={loaded}, result={result}, side={sys.modules[__name__]._side}')
loaded=True, result=default, side=['loaded']
r1-p007 · Mutable State Re-evaluation
1state = [True]
2
3def get_flag():
4 return state.pop() if state else False
5
6a = get_flag() # first call: True, state becomes []
7b = 'first'
8c = 'second'
9# Rule fires: b if not a else c
10# Original: not a -> not True -> False -> pick c -> 'second'
11# After fix: a if a else c -> True -> pick a -> 'first' (but a is already evaluated!)
12# Wait: we need to make sure a is re-evaluated in fix.
13# Let's restructure so a is a function call.
14print(f'a={a}')
15result = b if not get_flag() else c # get_flag() called again -> returns False
16print(f'result={result}')
a=True result=first
r1-p008 · File Write Truncation Hazard
1with open('test.txt', 'w') as f:
2 f.write('initial')
3
4with open('test.txt', 'r+') as f:
5 f.seek(0)
6 data = f.read(1) # read 'i'
7 # Rule fires: f.write(data) if not data else f.write('X')
8 # But we want to see if fix changes behavior
9 # Instead: use ternary to decide what to write
10 to_write = data if not data else 'X' # data='i' -> truthy -> not data=False -> pick else: 'X'
11 f.write(to_write)
12 f.seek(0)
13 content = f.read()
14 print(f'content={repr(content)}')
15import os
16os.remove('test.txt')
content='initialX'
r2-p001 · Lambda captures changed by duplicate evaluation
1def outer():
2 x = []
3 vals = [lambda i=i: x.append(i) for i in range(2)]
4 f = vals[0] if not len(x) else vals[1]
5 return f()
6outer()
7print(len(x))
r2-p002 · Shadowed built-in after redefinition
1list = lambda: [99]
2x = list() if not False else list()
3print(x)
[99]
r2-p003 · Side effect duplicated in fix
1state = 0
2def get():
3 global state
4 state += 1
5 return state
6a = get() if not False else 0
7print(a, state)
1 1
r2-p004 · Keyword-only arg default re-evaluated
1def f(*, default=[]):
2 default.append(1)
3 return default
4x = f() if not False else f()
5print(len(x))
1
r2-p005 · Exception raised in condition changes timing
1def bad():
2 raise ValueError('fail')
3try:
4 x = 1 if not False else bad()
5except ValueError as e:
6 print('caught', e)
r2-p006 · Property setter triggered twice
1class C:
2 def __init__(self):
3 self._v = 0
4 self.count = 0
5 @property
6 def v(self):
7 return self._v
8 @v.setter
9 def v(self, val):
10 self._v = val
11 self.count += 1
12c = C()
13_.v = c.v if not False else 5
14print(c.count)
r2-p007 · Iterator consumed twice
1def gen():
2 yield 1
3 yield 2
4g = gen()
5first = next(g) if not False else next(g)
6print(first)
1
r2-p008 · Binary mode file write truncation hazard
1with open('test.bin', 'wb') as f:
2 pass
3data = b'hello' if not True else b'world'
4with open('test.bin', 'wb') as f:
5 f.write(data)
6with open('test.bin', 'rb') as f:
7 print(f.read())
8import os
9os.remove('test.bin')
b'world'