fixhunt

ruff FURB132

labelled unknown
Runs / 2026-10-17T21-31-58-398Z-gtky-ruff-FURB132ruff 0.17.12026-10-17 21:31 UTC8 programs × 2 round(s)16 programs, 8 fired, 0 fixed, 0 findings, 1 rejectedmodel cost $0.0040

Analysis Nemotron 3 Super

Flags conditional removal via 'in' followed by .remove() where discard() would suffice.

Fix: Replace the conditional block checking membership and calling .remove() with a direct call to .discard() on the same set with the same argument.

12 hazards
  • The condition may have side effects (e.g., function calls) whose execution is skipped when replacing with discard(), altering program behavior.
  • If the set expression is complex (e.g., a function call), evaluating it once in discard() vs. twice in the original (in condition and remove) changes evaluation count and potential side effects.
  • The variable used in the condition and remove might be reassigned or aliased between the check and the removal, making discard() operate on a different value.
  • If the set is a property or item access (e.g., obj.attr or arr[idx]), the fix may change how many times the attribute/index is evaluated.
  • The original code may rely on the KeyError being raised for debugging or control flow when the element is absent, which discard() suppresses.
  • If the set is actually a frozenset or other immutable set-like type, discard() may not exist or behave differently, causing AttributeError.
  • The fix may remove necessary comments or formatting attached to the if statement or its body that are not preserved in the rewrite.
  • In nested scopes, the variable names might refer to different entities due to shadowing, and the fix does not re-resolve them in the new context.
  • If the set is accessed via a descriptor (__getitem__/__setitem__) with logging or validation, discard() bypasses the intermediate check logic.
  • The rewrite assumes the argument to remove is hashable and valid for discard; if it is unhashable, both fail, but the error type or message might differ slightly in edge cases.
  • If the set variable is reused later and the conditional removal was part of a larger state-dependent logic, discarding unconditionally may break invariants.
  • The fix may interfere with debugger breakpoints set on the if line or the remove line, altering debugging experience.

Programs

idresulttitlehazard
r1-p001fired, no fixGenerators vs Lists - Iterator Consumptiongenerators vs lists, iterators consumed once, lazy vs eager evaluation
r1-p002silent prediction offVariable Reassignment Between Check and RemoveThe variable used in the condition and remove might be reassigned or aliased between the check and the removal, making discard() operate on a different value.
r1-p003silent prediction offMutable Default Arguments and Aliasingmutable default arguments and aliasing after a rewrite
r1-p004fired, no fix prediction offReliance on KeyError for Control FlowThe original code may rely on the KeyError being raised for debugging or control flow when the element is absent, which discard() suppresses.
r1-p005fired, no fixNested Scopes and Variable ShadowingIn nested scopes, the variable names might refer to different entities due to shadowing, and the fix does not re-resolve them in the new context.
r1-p006fired, no fixNumeric Coercion and Type Semanticsint/float/bool coercion, Decimal, division and rounding
r1-p007silent prediction offState-Dependent Logic and Set ReuseIf the set variable is reused later and the conditional removal was part of a larger state-dependent logic, discarding unconditionally may break invariants.
r1-p008fired, no fixString Formatting Edge Cases in Contextstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p001silent prediction offProperty Access Evaluation CountIf the set is a property or item access (e.g., obj.attr or arr[idx]), the fix may change how many times the attribute/index is evaluated.
r2-p002silent prediction offFrozenset Immutable TypeIf the set is actually a frozenset or other immutable set-like type, discard() may not exist or behave differently, causing AttributeError.
r2-p003silent prediction offComplex Set Expression Side EffectsIf the set expression is complex (e.g., a function call), evaluating it once in discard() vs. twice in the original (in condition and remove) changes evaluation count and potential side effects.
r2-p004fired, no fix prediction offTruthiness and Custom Booleantruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r2-p005silent prediction offMutable Default Arguments and Aliasingmutable default arguments and aliasing after a rewrite
r2-p006fired, no fixUnhashable Argument Error DifferenceThe rewrite assumes the argument to remove is hashable and valid for discard; if it is unhashable, both fail, but the error type or message might differ slightly in edge cases.
r2-p007invalidShadowed Builtinsshadowed builtins and names redefined later in the module
r2-p008fired, no fixDebugger Breakpoint InterferenceThe fix may interfere with debugger breakpoints set on the if line or the remove line, altering debugging experience.

Sources

r1-p001 · Generators vs Lists - Iterator Consumption
1def gen():
2 yield 1
3 yield 2
4 yield 3
5s = {1, 2, 3}
6it = gen()
7for _ in range(3):
8 x = next(it)
9 if x in s:
10 s.remove(x)
11print('Final set:', s)
Final set: set()
r1-p002 · Variable Reassignment Between Check and Remove
1x = 1
2s = {1, 2}
3if x in s:
4 x = 2 # reassigned before remove
5 s.remove(x) # removes 2, not original 1
6print('Set after:', s)
7print('x:', x)
Set after: {1}
x: 2
r1-p003 · Mutable Default Arguments and Aliasing
1def f(x, s={1, 2, 3}):
2 if x in s:
3 s.remove(x)
4 return s
5print('First call:', f(2))
6print('Second call:', f(1)) # s is mutated across calls
First call: {1, 3}
Second call: {3}
r1-p004 · Reliance on KeyError for Control Flow
1s = {1, 2, 3}
2try:
3 if 4 in s:
4 s.remove(4)
5except KeyError as e:
6 print('Caught KeyError:', e)
7else:
8 print('No exception, set unchanged:', s)
No exception, set unchanged: {1, 2, 3}
r1-p005 · Nested Scopes and Variable Shadowing
1def outer():
2 x = 1
3 s = {1, 2}
4 def inner():
5 x = 2 # shadows outer x
6 if x in s: # refers to inner x=2
7 s.remove(x) # removes 2
8 inner()
9 print('Set after inner:', s)
10 print('Outer x:', x)
11outer()
Set after inner: {1}
Outer x: 1
r1-p006 · Numeric Coercion and Type Semantics
1s = {1, 2, 3}
2x = 1.0 # float, but equals int 1
3if x in s:
4 s.remove(x) # removes int 1 due to hash equality
5print('Set after remove(1.0):', s)
6print('Type of elements:', [type(e) for e in s])
Set after remove(1.0): {2, 3}
Type of elements: [<class 'int'>, <class 'int'>]
r1-p007 · State-Dependent Logic and Set Reuse
1s = {1, 2, 3}
2removed = []
3if 2 in s:
4 s.remove(2)
5 removed.append(2)
6if len(s) == 2: # invariant: expected size after removal
7 print('Invariant holds')
8else:
9 print('Invariant broken:', s)
10print('Removed list:', removed)
Invariant holds
Removed list: [2]
r1-p008 · String Formatting Edge Cases in Context
1s = {'hello', 'world'}
2val = 'hello'
3if val in s:
4 s.remove(val)
5print('Remaining:', ', '.join(s))
6print('f-string: %s' % ', '.join(s))
7print('format: {}'.format(', '.join(s)))
Remaining: world
f-string: world
format: world
r2-p001 · Property Access Evaluation Count
1class C:
2 def __init__(self):
3 self.counter = 0
4 self.s = {1, 2}
5 @property
6 def attr(self):
7 self.counter += 1
8 return self.s
9obj = C()
10print('Start:', obj.counter)
11if 1 in obj.attr:
12 obj.attr.remove(1)
13print('After if-remove:', obj.counter)
14# Reset counter to see second evaluation
15obj.counter = 0
16if 2 in obj.attr:
17 obj.attr.remove(2)
18print('After second if-remove:', obj.counter)
Start: 0
After if-remove: 2
After second if-remove: 2
r2-p002 · Frozenset Immutable Type
1s = frozenset({1, 2, 3})
2try:
3 if 2 in s:
4 s.remove(2)
5except AttributeError as e:
6 print('AttributeError:', type(e).__name__, ':', e)
7# Now with discard - this will also fail
8try:
9 if 2 in s:
10 s.discard(2)
11except AttributeError as e:
12 print('discard AttributeError:', type(e).__name__, ':', e)
AttributeError: AttributeError : 'frozenset' object has no attribute 'remove'
discard AttributeError: AttributeError : 'frozenset' object has no attribute 'discard'
r2-p003 · Complex Set Expression Side Effects
1counter = 0
2
3def get_set():
4 global counter
5 counter += 1
6 return {1, 2, 3}
7s = get_set()
8print('Initial counter:', counter)
9counter = 0
10if 1 in get_set():
11 get_set().remove(1)
12print('After if-remove calls:', counter)
13counter = 0
14# This simulates what the fix would do: one call to get_set
15# But we can't run the fix, so we show the difference in evaluation
16# Original: two calls to get_set() -> counter increases by 2
17# Fixed: one call -> counter increases by 1
18# We'll show by manually doing both
19s1 = get_set() # call 1
20if 1 in s1:
21 s2 = get_set() # call 2
22 s2.remove(1)
23print('Two calls result:', counter)
24counter = 0
25s = get_set() # call 1
26if 1 in s:
27 s.discard(1) # no second call
28print('One call result (simulated fix):', counter)
Initial counter: 1
After if-remove calls: 2
Two calls result: 2
One call result (simulated fix): 1
r2-p004 · Truthiness and Custom Boolean
1class MyBool:
2 def __init__(self, val):
3 self.val = val
4 def __bool__(self):
5 return bool(self.val)
6 def __eq__(self, other):
7 return self.val == other
8x = MyBool(0)
9s = {x}
10print('x is False:', x is False)
11print('x == False:', x == False)
12print('bool(x):', bool(x))
13if x in s: # This checks equality, not identity
14 s.remove(x)
15print('Set after remove:', s)
16# Now test with None
17s2 = {None}
18if None in s2:
19 s2.remove(None)
20print('Set with None after remove:', s2)
r2-p005 · Mutable Default Arguments and Aliasing
1def f(x, s={1, 2, 3}):
2 if x in s:
3 s.remove(x)
4 return s
5print('First call f(1):', f(1))
6print('Second call f(2):', f(2))
7print('Third call f(3):', f(3))
8print('Fourth call f(1):', f(1)) # 1 is already removed
9# Show aliasing
10s = {1, 2, 3}
11alias = s
12if 1 in alias:
13 alias.remove(1)
14print('After alias remove, s:', s, 'alias:', alias)
First call f(1): {2, 3}
Second call f(2): {3}
Third call f(3): set()
Fourth call f(1): set()
After alias remove, s: {2, 3} alias: {2, 3}
r2-p006 · Unhashable Argument Error Difference
1lst = [1, 2]
2s = {1, 2, 3}
3try:
4 if lst in s:
5 s.remove(lst)
6except TypeError as e:
7 print('remove TypeError:', type(e).__name__, ':', e)
8try:
9 if lst in s:
10 s.discard(lst)
11except TypeError as e:
12 print('discard TypeError:', type(e).__name__, ':', e)
13# Note: In practice, both give same error, but we show the pattern
14# Let's make a case where the error might differ in custom class
15class Unhashable:
16 def __eq__(self, other):
17 return True
18 def __hash__(self):
19 raise TypeError('unhashable')
20u = Unhashable()
21s2 = {1, 2, 3}
22try:
23 if u in s2:
24 s2.remove(u)
25except TypeError as e:
26 print('Custom unhashable remove:', type(e).__name__, ':', e)
27try:
28 if u in s2:
29 s2.discard(u)
30except TypeError as e:
31 print('Custom unhashable discard:', type(e).__name__, ':', e)
remove TypeError: TypeError : cannot use 'list' as a set element (unhashable type: 'list')
discard TypeError: TypeError : cannot use 'list' as a set element (unhashable type: 'list')
Custom unhashable remove: TypeError : cannot use 'Unhashable' as a set element (unhashable)
Custom unhashable discard: TypeError : cannot use 'Unhashable' as a set element (unhashable)
r2-p007 · Shadowed Builtins (generated program does not parse: Traceback (most recent call last):)
1remove = lambda x: print('shadowed remove called with', x)
2discard = lambda x: print('shadowed discard called with', x)
3s = {1, 2, 3}
4x = 2
5if x in s:
6 remove(x) # This calls the shadowed remove, not set.remove
7print('After shadowed remove, s unchanged:', s)
8# Now if we had used discard via fix, it would call shadowed discard
9# But to show the hazard, we demonstrate what the fix would change
10# Reset and show what happens with real methods
11 del remove, discard # remove shadowing
12if x in s:
13 s.remove(x) # real set.remove
14print('After real remove:', s)
15s.add(x) # add back
16if x in s:
17 s.discard(x) # real set.discard
18print('After real discard:', s)
r2-p008 · Debugger Breakpoint Interference
1s = {1, 2, 3}
2# Simulate debugger behavior by tracing execution
3lines_executed = []
4def trace_func(frame, event, arg):
5 if event == 'line':
6 lines_executed.append(frame.f_lineno)
7 return trace_func
8import sys
9sys.settrace(trace_func)
10try:
11 if 2 in s:
12 s.remove(2)
13finally:
14 sys.settrace(None)
15print('Lines executed:', lines_executed)
16print('Set after:', s)
17# Now reset and show what would happen with discard (one line less)
18s = {1, 2, 3}
19lines_executed = []
20sys.settrace(trace_func)
21try:
22 if 2 in s:
23 s.discard(2) # This is what the fix would produce
24finally:
25 sys.settrace(None)
26print('Lines executed with discard:', lines_executed)
27print('Set after discard:', s)
Lines executed: []
Set after: {1, 3}
Lines executed with discard: []
Set after discard: {1, 3}