ruff RUF051
labelled unknown
Analysis Nemotron 3 Super
Flags 'if key in dict: del dict[key]' patterns that can be replaced with dict.pop(key, None).
Fix: Replaces 'if key in dictionary: del dictionary[key]' with 'dictionary.pop(key, None)', preserving indentation and removing the conditional block.
10 hazards
- The original code only deletes the key when present; pop(key, None) silently ignores missing keys but otherwise behaves identically when the key exists, so no behavioral change occurs for existing keys.
- If the dictionary expression has side effects (e.g., a function call), evaluating it twice in the condition and once in the fix could change behavior if the function mutates state or returns different values.
- If the key expression has side effects (e.g., a function call), evaluating it twice in the original versus once in the fix alters the number of side effects.
- If the dictionary or key expression raises an exception during evaluation, the original may raise it in the condition while the fix may raise it during the pop call, changing when the exception occurs.
- If the dictionary is aliased elsewhere, both versions mutate the same object, so no difference in aliasing behavior.
- If the del statement is inside a try-except block that catches KeyError, replacing it with pop may silence the exception where the original would have propagated it (though the condition prevents KeyError in the original).
- If the block contains comments, the fix may discard them as the rule marks fixes unsafe when comments are present in the if statement.
- If the dictionary is a property or descriptor with __delitem__ side effects, pop may invoke different internal logic than direct deletion, potentially altering behavior.
- If the key is unhashable, both the condition and the del/pop will raise TypeError at the same point, so no difference in error timing.
- If the code relies on the KeyError being raised for missing keys (e.g., in a broader try-except), the original condition prevents entry so no error occurs, matching pop's silent ignore.
Findings
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r2-p001 | fix-changes-behavior | labelled safe | real · high | related | The rule rewrites `if key in dictionary: del dictionary[key]` to `dictionary.pop(key, None)` assuming the key is hashable. In the program, `lst` is an unhashab… |
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Dictionary expression with side effects | If the dictionary expression has side effects (e.g., a function call), evaluating it twice in the condition and once in the fix could change behavior if the function mutates state or returns different |
| r1-p002 | silent | Del inside try-except catching KeyError | If the del statement is inside a try-except block that catches KeyError, replacing it with pop may silence the exception where the original would have propagated it (though the condition prevents KeyE |
| r1-p003 | silent prediction off | Mutable default arguments and aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p004 | fix ok | Reliance on KeyError being raised for missing keys | If the code relies on the KeyError being raised for missing keys (e.g., in a broader try-except), the original condition prevents entry so no error occurs, matching pop's silent ignore. |
| r1-p005 | silent prediction off | Dictionary or key expression raises exception during evaluation | If the dictionary or key expression raises an exception during evaluation, the original may raise it in the condition while the fix may raise it during the pop call, changing when the exception occurs |
| r1-p006 | fired, no fix | Block contains comments | If the block contains comments, the fix may discard them as the rule marks fixes unsafe when comments are present in the if statement. |
| r1-p007 | silent prediction off | Truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r1-p008 | silent prediction off | Evaluation order and side effects when expressions are merged, duplicated or deleted | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p001 | fix-changes-behavior | Unhashable key - identical error timing | 1. If the key is unhashable, both the condition and the del/pop will raise TypeError at the same point, so no difference in error timing. |
| r2-p002 | silent prediction off | Comment inside if-block discarded by fix | 2. If the block contains comments, the fix may discard them as the rule marks fixes unsafe when comments are present in the if statement. |
| r2-p003 | silent prediction off | String formatting edge cases in key expression | 3. string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p004 | silent prediction off | Dictionary property with __delitem__ side effects | 4. If the dictionary is a property or descriptor with __delitem__ side effects, pop may invoke different internal logic than direct deletion, potentially altering behavior. |
| r2-p005 | silent prediction off | Key expression with side effects evaluated twice vs once | 5. If the key expression has side effects (e.g., a function call), evaluating it twice in the original versus once in the fix alters the number of side effects. |
| r2-p006 | silent prediction off | Exceptions inside rewritten code with finally/else | 6. exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p007 | fix ok | Shadowed builtins and redefined names | 7. shadowed builtins and names redefined later in the module |
| r2-p008 | silent prediction off | Generator vs list consumption in key expression | 8. generators vs lists, iterators consumed once, lazy vs eager evaluation |
Sources
r1-p001 · Dictionary expression with side effects
1def get_dict():
2 global counter
3 counter += 1
4 return {'a': 1}
5counter = 0
6d = get_dict()
7if 'a' in get_dict(): # Rule triggers here
8 del get_dict()['a']
9print('counter:', counter)
counter: 3
r1-p002 · Del inside try-except catching KeyError
1try:
2 if 'missing' in d:
3 del d['missing']
4except KeyError as e:
5 print('Caught:', type(e).__name__, str(e))
6d = {}
7# This block does not trigger the rule because it's inside try-except
8# but we include it to show context; no trigger expected
r1-p003 · Mutable default arguments and aliasing
1def f(d={'x': 1}):
2 if 'x' in d:
3 del d['x']
4 return d
5print('First call:', f())
6print('Second call:', f())
First call: {}
Second call: {}r1-p004 · Reliance on KeyError being raised for missing keys
1d = {}
2try:
3 if 'key' in d:
4 del d['key']
5except KeyError:
6 print('This should not print')
7else:
8 print('No error')
--- orig.py 2026-10-17 17:47:06.738449602 -0400 +++ fixed.py 2026-10-17 17:47:06.901483294 -0400 @@ -1,7 +1,6 @@ d = {} try: - if 'key' in d: - del d['key'] + d.pop('key', None) except KeyError: print('This should not print') else:
No error
r1-p005 · Dictionary or key expression raises exception during evaluation
1def bad_key():
2 raise ValueError('bad key')
3d = {'a': 1}
4try:
5 if bad_key() in d:
6 del d[bad_key()]
7except ValueError as e:
8 print('Caught in condition:', type(e).__name__, str(e))
Caught in condition: ValueError bad key
r1-p006 · Block contains comments
1d = {'a': 1}
2if 'a' in d: # Keep this comment
3 del d['a'] # And this one too
4print('d:', d)
d: {}r1-p007 · Truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
1class FakeDict:
2 def __contains__(self, key):
3 return key == 'x'
4 def __delitem__(self, key):
5 raise RuntimeError('deleted')
6d = FakeDict()
7try:
8 if 'x' in d:
9 del d['x']
10except RuntimeError as e:
11 print('Caught:', type(e).__name__, str(e))
Caught: RuntimeError deleted
r1-p008 · Evaluation order and side effects when expressions are merged, duplicated or deleted
1l = []
2def side_effect():
3 l.append(1)
4 return {'a': 1}
5d = side_effect()
6if 'a' in side_effect(): # side_effect called twice
7 del side_effect()['a'] # and again
8print('side_effect calls:', len(l))
side_effect calls: 3
r2-p001 · Unhashable key - identical error timing
1lst = [1]
2dct = {}
3try:
4 if lst in dct: del dct[lst]
5except Exception as e:
6 print(f'{type(e).__name__}: {e}')
7try:
8 if lst in dct: del dct[lst]
9except Exception as e:
10 print(f'{type(e).__name__}: {e}')
--- orig.py 2026-10-17 17:47:13.630865416 -0400 +++ fixed.py 2026-10-17 17:47:13.800900155 -0400 @@ -1,10 +1,10 @@ lst = [1] dct = {} try: - if lst in dct: del dct[lst] + dct.pop(lst, None) except Exception as e: print(f'{type(e).__name__}: {e}') try: - if lst in dct: del dct[lst] + dct.pop(lst, None) except Exception as e: print(f'{type(e).__name__}: {e}')
TypeError: cannot use 'list' as a dict key (unhashable type: 'list') TypeError: cannot use 'list' as a dict key (unhashable type: 'list')
r2-p002 · Comment inside if-block discarded by fix
1log = []
2cache = {'x': 1}
3def record(msg):
4 log.append(msg)
5if 'x' in cache:
6 # keep this note
7 del cache['x']
8 record('deleted')
9print('log:', log)
10print('cache:', cache)
log: ['deleted']
cache: {}r2-p003 · String formatting edge cases in key expression
1def fmt(k): return f'id-{k}'
2data = {'id-A': 10, 'id-B': 20}
3k = 'A'
4key_expr = fmt(k)
5if key_expr in data:
6 val = data.pop(key_expr, None)
7 print(f'popped {key_expr} -> {val}')
8print('remaining:', data)
popped id-A -> 10
remaining: {'id-B': 20}r2-p004 · Dictionary property with __delitem__ side effects
1class Store:
2 def __init__(self):
3 self._log = []
4 self._data = {'a': 1}
5 @property
6 def container(self):
7 return self._data
8 def __delitem__(self, key):
9 self._log.append(f'del {key}')
10 super().__delitem__(key)
11s = Store()
12if 'a' in s.container:
13 del s.container['a']
14print('log:', s._log)
15print('data:', s._data)
log: []
data: {}r2-p005 · Key expression with side effects evaluated twice vs once
1counter = 0
2def next_key():
3 global counter
4 counter += 1
5 return str(counter)
6store = {}
7if next_key() in store:
8 del store[next_key()]
9print('counter after if-del:', counter)
10counter = 0
11store = {}
12if next_key() in store:
13 del store[next_key()]
14print('counter after if-del (second):', counter)
counter after if-del: 1 counter after if-del (second): 1
r2-p006 · Exceptions inside rewritten code with finally/else
1log = []
2def risky_pop(d, k):
3 log.append('pop-enter')
4 if k == 'bad':
5 raise ValueError('poison')
6 return d.pop(k, None)
7d = {'good': 1, 'bad': 2}
8try:
9 if 'good' in d:
10 val = risky_pop(d, 'good')
11 log.append(f'got {val}')
12except ValueError as e:
13 log.append(f'bad-value: {e}')
14else:
15 log.append('else: ok')
16finally:
17 log.append('finally')
18print('|'.join(log))
pop-enter|got 1|else: ok|finally
r2-p007 · Shadowed builtins and redefined names
1pop = lambda d, k=None: [None][0]
2d = {'a': 1}
3if 'a' in d:
4 del d['a']
5print('after del:', d)
6if 'a' in d:
7 pop(d, 'a')
8print('after pop:', d)
--- orig.py 2026-10-17 17:47:14.531049152 -0400 +++ fixed.py 2026-10-17 17:47:14.694082406 -0400 @@ -1,7 +1,6 @@ pop = lambda d, k=None: [None][0] d = {'a': 1} -if 'a' in d: - del d['a'] +d.pop('a', None) print('after del:', d) if 'a' in d: pop(d, 'a')
after del: {}
after pop: {}r2-p008 · Generator vs list consumption in key expression
1def gen_keys():
2 yield 'x'
3 yield 'y'
4k_iter = gen_keys()
5container = {'x': 1, 'y': 2}
6try:
7 if next(k_iter) in container:
8 del container[next(k_iter)]
9except StopIteration:
10 pass
11print('remaining:', container)
12print('exhausted:', list(gen_keys()))
remaining: {'x': 1}
exhausted: ['x', 'y']