fixhunt

ruff RUF051

labelled unknown
Runs / 2026-10-17T21-46-58-522Z-boj8-ruff-RUF051ruff 0.17.12026-10-17 21:46 UTC8 programs × 2 round(s)16 programs, 4 fired, 3 fixed, 1 findings, 0 rejectedmodel cost $0.02

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

programoraclelabeljudgestatussummary
r2-p001fix-changes-behaviorlabelled safereal · highrelatedThe 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

idresulttitlehazard
r1-p001silent prediction offDictionary expression with side effectsIf 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-p002silentDel inside try-except catching KeyErrorIf 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-p003silent prediction offMutable default arguments and aliasingmutable default arguments and aliasing after a rewrite
r1-p004fix okReliance on KeyError being raised for missing keysIf 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-p005silent prediction offDictionary or key expression raises exception during evaluationIf 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-p006fired, no fixBlock contains commentsIf the block contains comments, the fix may discard them as the rule marks fixes unsafe when comments are present in the if statement.
r1-p007silent prediction offTruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r1-p008silent prediction offEvaluation order and side effects when expressions are merged, duplicated or deletedevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p001fix-changes-behaviorUnhashable key - identical error timing1. 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-p002silent prediction offComment inside if-block discarded by fix2. 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-p003silent prediction offString formatting edge cases in key expression3. string formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p004silent prediction offDictionary property with __delitem__ side effects4. 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-p005silent prediction offKey expression with side effects evaluated twice vs once5. 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-p006silent prediction offExceptions inside rewritten code with finally/else6. exceptions raised inside the rewritten code, finally/else clauses, exception chaining
r2-p007fix okShadowed builtins and redefined names7. shadowed builtins and names redefined later in the module
r2-p008silent prediction offGenerator vs list consumption in key expression8. 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']