ruff FURB132
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Generators vs Lists - Iterator Consumption | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r1-p002 | silent prediction off | Variable Reassignment Between Check and Remove | 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. |
| r1-p003 | silent prediction off | Mutable Default Arguments and Aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p004 | fired, no fix prediction off | Reliance on KeyError for Control Flow | The original code may rely on the KeyError being raised for debugging or control flow when the element is absent, which discard() suppresses. |
| r1-p005 | fired, no fix | Nested Scopes and Variable Shadowing | 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. |
| r1-p006 | fired, no fix | Numeric Coercion and Type Semantics | int/float/bool coercion, Decimal, division and rounding |
| r1-p007 | silent prediction off | State-Dependent Logic and Set Reuse | If the set variable is reused later and the conditional removal was part of a larger state-dependent logic, discarding unconditionally may break invariants. |
| r1-p008 | fired, no fix | String Formatting Edge Cases in Context | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p001 | silent prediction off | Property Access Evaluation Count | 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. |
| r2-p002 | silent prediction off | Frozenset Immutable Type | If the set is actually a frozenset or other immutable set-like type, discard() may not exist or behave differently, causing AttributeError. |
| r2-p003 | silent prediction off | Complex Set Expression Side Effects | 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. |
| r2-p004 | fired, no fix prediction off | Truthiness and Custom Boolean | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r2-p005 | silent prediction off | Mutable Default Arguments and Aliasing | mutable default arguments and aliasing after a rewrite |
| r2-p006 | fired, no fix | Unhashable Argument Error Difference | 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. |
| r2-p007 | invalid | Shadowed Builtins | shadowed builtins and names redefined later in the module |
| r2-p008 | fired, no fix | Debugger Breakpoint Interference | The 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: 2r1-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: 1r1-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}