fixhunt

ruff C403

labelled unknown
Runs / 2026-10-17T20-17-51-416Z-qrzl-ruff-C403ruff 0.17.12026-10-17 20:17 UTC8 programs × 2 round(s)8 programs, 4 fired, 0 fixed, 0 findings, 3 rejectedmodel cost $0.0057

Analysis Nemotron 3 Super

Flags set() calls wrapping list comprehensions where the elements do not depend on iteration variables.

Fix: Rewrites set([expr for vars in iters if conds]) into {expr for vars in iters if conds}, preserving the comprehension's structure but changing the container from list to set literal.

10 hazards
  • The rewrite may drop inline or block comments attached to the opening bracket, closing bracket, or within the list literal, as Ruff's autofix does not preserve them reliably.
  • If the list comprehension is used elsewhere in the same scope (e.g., assigned to a variable before passing to set()), the refactor breaks that usage by eliminating the list entirely.
  • In rare cases involving ambiguous grammar (e.g., set([...]) * 2), removing brackets could alter precedence and change the expression's meaning.
  • If the comprehension contains a walrus operator (:=) that binds a variable used after the set() call, the refactor may prematurely end the variable's scope or alter its lifetime.
  • When the list comprehension is part of a larger expression (e.g., in a return statement or argument list), the autofix might inadvertently affect formatting or line continuation in ways that break syntax.
  • If the code relies on the list being mutable (e.g., passed to a function that mutates it before set() consumes it), the refactor removes that mutation opportunity, though set() construction already makes this irrelevant—still, any intermediate mutation would be lost.
  • In Python versions prior to 3.6, set literals with comprehensions were not guaranteed to preserve insertion order, but since the original list comprehension also had no order guarantee for set(), this is not a behavioral change—however, if the code mistakenly assumed list order mattered for debugging or side-effect logging, the refactor could obscure intent.
  • If the list comprehension is written across multiple lines with trailing commas or hanging indentation, the autofix may normalize whitespace in a way that conflicts with project style guides (e.g., black vs. Ruff), causing formatting churn.
  • If the set() call is part of a macro-like pattern via eval or exec, the static rewrite could produce invalid code when the string is later executed.
  • If the comprehension includes a conditional expression that raises an exception during evaluation, the refactor does not change evaluation order or short-circuiting, so no semantic hazard arises—but if the list was being inspected for partial results before completion (e.g., in a debugger), that observation capability is lost.

Programs

idresulttitlehazard
r2-p001fired, no fixclass attributes vs instance attributes, descriptors, properties with side effectsDescriptor __get__ called twice due to double evaluation in set([...]) versus single evaluation in {...}; prints show descriptor access count.
r2-p002fired, no fixint/float/bool coercion, Decimal, division and roundingDecimal context changes precision mid-comprehension; list vs set construction timing affects when context is read.
r2-p003invalidwalrus operator binding lifetimeWalrus-assigned variable leaked from list comprehension scope in Python 3.8+, but set comprehension does not leak it—refactor removes accidental leak.
r2-p004invalidset literals insertion order pre-3.6 (debugging/logging intent obscured)Code assumes set construction order reflects list order for logging; refactor obscures that list order was ever considered relevant.
r2-p005invalidconditional expression raising exception during evaluationException in conditional expression occurs at same time in both forms; no semantic change, but intermediate list for debugging is lost.
r2-p006silent prediction offset() call in macro-like pattern via eval or execStatic rewrite produces invalid code when string contains set([...]) that is later executed via eval/exec.
r2-p007fired, no fixcode relies on list being mutable before set() consumptionIntermediate list mutation opportunity removed; though set() construction makes it irrelevant, any mutation during list build would be lost.
r2-p008fired, no fixevaluation order and side effects when expressions are merged, duplicated or deletedEvaluation order unchanged, but if expression had side effects that depended on temporary list state (e.g., caching), refactor removes intermediate storage.

Sources

r2-p001 · class attributes vs instance attributes, descriptors, properties with side effects
1class T:
2 def __init__(self):
3 self.x = None
4 def __get__(self, obj, owner):
5 return f'got-{id(obj)}'
6class Descriptor:
7 value = 0
8 def __get__(self, obj, owner):
9 self.value += 1
10 return [self.value]
11d = Descriptor()
12s = set([d.value for _ in [0]]) # Triggers rule; d.value accessed once? But set builds list first.
13print('list-based:', s)
14d = Descriptor()
15s2 = {d.value for _ in [0]} # After fix; still one access.
16print('set-literal:', s2)
17# Hazard: if descriptor had side effect on obj state, list build might retain ref? Not here.
18# Instead, make descriptor mutate owner class attr.
19class Meta(type):
20 log = []
21 def __get__(cls, obj, owner):
22 cls.log.append(id(cls))
23 return 42
24class C(metaclass=Meta):
25 pass
26print('pre-fix accesses:', C.Meta.log)
27s = set([C.Meta for _ in [0, 1]]) # Two accesses via list comp
28print('post list-comp:', C.Meta.log)
29s2 = {C.Meta for _ in [0, 1]} # Two accesses via set comp
30print('post set-comp:', C.Meta.log)
list-based: {0}
set-literal: {0}
r2-p002 · int/float/bool coercion, Decimal, division and rounding
1from decimal import Decimal, getcontext
2getcontext().prec = 28
3def gen():
4 getcontext().prec = 2 # Lower precision during iteration
5 yield Decimal('1') / Decimal('3')
6 getcontext().prec = 28 # Restore
7 yield Decimal('2') / Decimal('3')
8g = gen()
9s1 = set([str(x) for x in g]) # List built: first two yields seen at low prec, then high? No: gen exhausted in list comp.
10print('list-based:', s1)
11g = gen() # Reset
12s2 = {str(x) for x in g} # Set comp: same exhaustion order
13print('set-literal:', s2)
14# Hazard: if context read during set() call vs during iter? Both same.
15# Instead: make expression depend on mutable global that changes during iteration.
16precision_log = []
17def get_prec():
18 precision_log.append(getcontext().prec)
19 return getcontext().prec
20class Proxy:
21 def __init__(self, v): self.v = v
22 def __truediv__(self, other):
23 return Decimal(self.v) / Decimal(other.v)
24 def __str__(self): return str(self.v)
25getcontext().prec = 10
26items = [Proxy('1'), Proxy('2')]
27def gen_items():
28 for i in items:
29 getcontext().prec += 2 # Mutate during iteration
30 yield i
31s1 = set([str(x / Proxy('3')) for x in gen_items()]) # List comp: prec logged during build
32print('prec log (list):', precision_log)
33print('result:', s1)
34precision_log.clear()
35s2 = {str(x / Proxy('3')) for x in gen_items()} # Set comp: same log order
36print('prec log (set):', precision_log)
37print('result:', s2)
list-based: {'0.33', '0.6666666666666666666666666667'}
set-literal: {'0.33', '0.6666666666666666666666666667'}
prec log (list): []
result: {'0.66666666666667', '0.333333333333'}
prec log (set): []
result: {'0.666666666666666667', '0.3333333333333333'}
r2-p003 · walrus operator binding lifetime (generated program does not parse: Traceback (most recent call last):)
1# Walrus in comprehension: target leaks in list comp scope (but not in set comp) in Py3.8+
2# However, in function scope, neither leaks. Use class scope to see leak.
3class ScopeTest:
4 pass
5# In class scope, list comprehension leaks walrus var; set comp does not.
6# Rule rewrites set([...]) -> {...}, removing leak.
7try:
8 ScopeTest.walrus_var = None # Initialize
9 s = set([(ScopeTest.walrus_var := i) for i in [1, 2, 3]])
10 print('After list-based set:', hasattr(ScopeTest, 'walrus_var'), ScopeTest.walrus_var)
11except Exception as e:
12 print('Error in list-based:', type(e).__name__, e)
13try:
14 ScopeTest.walrus_var = None # Reset
15 s2 = {(ScopeTest.walrus_var := i) for i in [1, 2, 3]} # After fix
16 print('After set-literal:', hasattr(ScopeTest, 'walrus_var'), ScopeTest.walrus_var)
17except Exception as e:
18 print('Error in set-literal:', type(e).__name__, e)
19# Output: list-based leaks walrus_var=3; set-literal does not (or leaks? Actually, in class scope, set comp also leaks?)
20# Correction: in class scope, both leak. Need function scope.
21# Redo in function.
22def test():
23 walrus_var = None
24 s = set([(walrus_var := i) for i in [1, 2, 3]])
25 return walrus_var # Unbound in Py3.8+ if leaked? Actually, list comp does NOT leak in function scope.
26# Walrus never leaks in function scope comprehensions. So hazard is misstated.
27# Actual hazard: walrus in condition? Use assignment expression in filter.
28# Ref: https://stackoverflow.com/questions/52854790/assignment-expression-in-list-comprehension
29# In function scope: walrus does NOT leak. So no hazard? But rule says "may prematurely end variable's scope"
30# Perhaps: if walrus used AFTER set() call, and list comp kept it alive longer? No.
31# Instead: make walrus bind a resource that needs cleanup; list comp holds ref longer via list.
32class Resource:
33 def __init__(self, id): self.id = id
34 def __del__(self): print(f'Resource {self.id} cleaned')
35def make_resources():
36 for i in range(2):
37 yield Resource(i)
38gen = make_resources()
39# List comp: holds full list in memory during set() call; resources live until set built.
40# Set comp: resources may be freed earlier? Not guaranteed.
41s1 = set([r for r in gen]) # List built first
42print('After list-based set creation')
43gen = make_resources()
44s2 = {r for r in gen} # Set comp
45print('After set-literal creation')
46# Print order of __del__ may differ.
r2-p004 · set literals insertion order pre-3.6 (debugging/logging intent obscured) (generated program is not deterministic)
1construction_order = []
2class Logger:
3 def __init__(self, name):
4 self.name = name
5 def __repr__(self):
6 return f'<{self.name}>'
7 def __hash__(self):
8 return hash(self.name)
9 def __eq__(self, other):
10 return isinstance(other, Logger) and self.name == other.name
11items = [Logger('a'), Logger('b'), Logger('c')]
12# Build set via list comp: order of evaluation is items order, but set discards duplicates.
13# No duplicates here, so set() will have arbitrary order, but list comp ran in order.
14s1 = set([str(obj.name) for obj in items]) # List comp: logs in items order via str()? No, str doesn't log.
15# Make __str__ log construction order.
16def log_name(obj):
17 construction_order.append(obj.name)
18 return obj.name
19s1 = set([log_name(obj) for obj in items]) # Logs a,b,c in order
20print('Construction order (list-based):', construction_order)
21construction_order.clear()
22s2 = {log_name(obj) for obj in items} # Logs a,b,c in order (same)
23print('Construction order (set-literal):', construction_order)
24# To see hazard: if code later iterated set expecting list order.
25# But set order is arbitrary; logging during construction same.
26# Hazard: developer thought set() preserved list order for debugging, but refactor makes it look intentional.
27# Add duplicate to force non-trivial set building.
28items = [Logger('a'), Logger('b'), Logger('a'), Logger('c')]
29construction_order.clear()
30s1 = set([log_name(obj) for obj in items]) # Logs a,b,a,c
31print('With dup - construction order (list-based):', construction_order)
32print('Result:', s1)
33construction_order.clear()
34s2 = {log_name(obj) for obj in items} # Logs a,b,a,c
35print('With dup - construction order (set-literal):', construction_order)
36print('Result:', s2)
37# Same logs. No behavioral change.
38# Instead: make hazard about relying on list() intermediate for inspection.
39# Example: debug prints list before set conversion.
40items = [1,2,3]
41temp = [x*2 for x in items] # Not rewritten
42print('Intermediate list (for debugging):', temp)
43s = set(temp) # Not a comprehension inside set()
44print('Final set:', s)
45# Rule doesn't fire here.
46# Make it fire: set([x*2 for x in items])
47construction_order.clear()
48def record(x):
49 construction_order.append(x)
50 return x*2
51items = [1,2,3]
52s1 = set([record(x) for x in items]) # Logs 1,2,3
53print('Intermediate values recorded (list-based):', construction_order)
54construction_order.clear()
55s2 = {record(x) for x in items} # Logs 1,2,3
56print('Intermediate values recorded (set-literal):', construction_order)
57# Still same.
58# Conclusion: no semantic hazard, but we output as required.
r2-p005 · conditional expression raising exception during evaluation (generated program is not deterministic)
1results = []
2exceptions_caught = []
3def maybe_fail(x):
4 if x == 2:
5 raise ValueError('fail at 2')
6 return x * 10
7items = [0, 1, 2, 3]
8try:
9 # List comp: builds list; exception during build stops list creation
10 s = set([maybe_fail(x) for x in items if x != 1]) # Fails at x=2
11 print('This should not print')
12except Exception as e:
13 exceptions_caught.append((type(e).__name__, str(e)))
14 print('Caught in list-based:', exceptions_caught[-1])
15# After fix: set comp fails same point.
16exceptions_caught.clear()
17try:
18 s2 = {maybe_fail(x) for x in items if x != 1}
19 print('This should not print')
20except Exception as e:
21 exceptions_caught.append((type(e).__name__, str(e)))
22 print('Caught in set-literal:', exceptions_caught[-1])
23# To see intermediate results: need to catch exception and inspect partial list.
24# But list comp inside set() is not accessible.
25# Instead: make conditional expression that may fail, but we wrap in try/except inside comp.
26def safe_maybe_fail(x):
27 try:
28 return maybe_fail(x)
29 except ValueError as e:
30 return f'caught: {e}'
31items = [0,1,2,3]
32s1 = set([safe_maybe_fail(x) for x in items]) # No exception escapes
33print('List-based result:', s1)
34s2 = {safe_maybe_fail(x) for x in items}
35print('Set-literal result:', s2)
36# Same.
37# Hazard: if code relied on catching exception OUTSIDE to get partial list.
38# Impossible because list is transient.
39# We output trace showing same behavior.
r2-p006 · set() call in macro-like pattern via eval or exec
1template = 'set([x*2 for x in range(3)])' # Pattern to be rewritten by rule if in source
2# But rule only fires on actual source, not strings.
3# Hazard: if developer writes code that generates set([...]) strings, and linter rewrites the template.
4# Example: linter runs on this file, sees set([...]) in string? No, rule ignores strings.
5# So we must put the pattern in actual code that gets eval'd, but rule rewrites it prematurely.
6# Make the string contain the pattern, and rule fires on it because it's in source.
7# Then when eval'd, it's invalid because set literal with comp requires braces.
8code_string = "set([x*2 for x in range(3)])" # This line triggers rule
9# After fix, becomes: {x*2 for x in range(3)}
10# But if this string is meant to be executed later, the rewritten version is wrong.
11# However, the rule rewrites the source, so the string literal changes.
12# We show: before fix, eval works; after fix, eval fails.
13# But we can't show both in one run.
14# Instead: demonstrate that rewritten string fails eval.
15try:
16 # This is the fixed version: {x*2 for x in range(3)} inside quotes
17 s = eval("{x*2 for x in range(3)}") # Valid set comp
18 print('Eval of set-literal string:', s)
19except Exception as e:
20 print('Eval failed:', type(e).__name__, e)
21# Now the broken version: set([x*2 for x in range(3)]) as string
22try:
23 s = eval("set([x*2 for x in range(3)])") # Valid
24 print('Eval of list-based string:', s)
25except Exception as e:
26 print('Eval failed:', type(e).__name__, e)
27# To show hazard: suppose we have a function that expects the old pattern.
28def process_pattern(pattern_str):
29 # Expects pattern that uses set([...])
30 return eval(pattern_str)
31# If linter rewrote the pattern_str in source, it breaks.
32pattern = "set([x*2 for x in range(3)])" # This line may be rewritten
33# After fix, pattern becomes "{x*2 for x in range(3)}"
34# But if process_pattern expects the old form, it still works because both are valid.
35# Actually, both forms are valid in eval.
36# Hazard only if string is used in context requiring list, not set.
37# Example: code that does list(set([...])) expects list intermediate.
38# But set([...]) is used to make set, not list.
39# We conclude: hazard is that eval(string) may break if string assumes list comp semantics.
40# But set([...]) and {...} both make sets.
41# Unless the string is part of a larger expression.
42# Let's use: "list(set([x for x in range(3)]))"
43# Rule does NOT fire on set([...]) inside list()? It does: set([...]) is inner.
44# After fix: list({x for x in range(3)}) which is valid.
45# Still works.
46# Give up and show that rewritten string is different.
47original = "set([x*2 for x in range(3)])"
48rewritten = "{x*2 for x in range(3)}"
49print('Original pattern:', original)
50print('Rewritten pattern:', rewritten)
51print('Are they equal?', original == rewritten)
52# Eval both:
53print('Eval original:', eval(original))
54print('Eval rewritten:', eval(rewritten))
Eval of set-literal string: {0, 2, 4}
Eval of list-based string: {0, 2, 4}
Original pattern: set([x*2 for x in range(3)])
Rewritten pattern: {x*2 for x in range(3)}
Are they equal? False
Eval original: {0, 2, 4}
Eval rewritten: {0, 2, 4}
r2-p007 · code relies on list being mutable before set() consumption
1mutations = []
2class MutableItem:
3 def __init__(self, value):
4 self.value = value
5 def __repr__(self):
6 return f'Item({self.value})'
7 def mutate(self):
8 self.value *= 10
9 mutations.append(id(self))
10items = [MutableItem(i) for i in range(3)]
11# During list comprehension build, we could mutate items if we had access.
12# But comp expression only sees each item.
13# Instead: make comprehension expression mutate a global.
14counter = [0]
15def increment_and_return(x):
16 counter[0] += 1
17 return x * counter[0]
18s1 = set([increment_and_return(i) for i in [1,2,3]]) # List comp: increment called 1,2,3 in order
19print('Mutation log (list-based):', counter[0])
20print('Result:', s1)
21counter[0] = 0
22s2 = {increment_and_return(i) for i in [1,2,3]} # Set comp: same call order
23print('Mutation log (set-literal):', counter[0])
24print('Result:', s2)
25# Same.
26# Hazard: if list was built and then mutated before set() call.
27# Example: lst = [expr for ...]; lst.append(...); s = set(lst)
28# Rule doesn't fire on that.
29# We need to make the list comp inside set() be the only reference, but imagine:
30# Developer assumes they can insert a debugging mutation between list creation and set() call.
31# After refactor, no intermediate list exists to mutate.
32# Show: if we could mutate the list during its construction (impossible).
33# Instead: use side effect in expression that depends on list length so far.
34# But list comp doesn't expose partial list.
35# We simulate by having expression read a global that tracks how many items processed.
36processed_count = []
37def process(x):
38 processed_count.append(len(processed_count)) # Index of this item
39 return x * 10
40items = [1,2,3]
41s1 = set([process(x) for x in items]) # processed_count: [0,1,2]
42print('Processed indices (list-based):', processed_count)
43print('Result:', s1)
44processed_count.clear()
45s2 = {process(x) for x in items} # processed_count: [0,1,2]
46print('Processed indices (set-literal):', processed_count)
47print('Result:', s2)
48# Same.
49# Final attempt: hazard is that list() constructor is explicit about creating list; set literal hides it.
50# For teaching, removing list() may obscure intent.
51# We output trace showing same behavior but note that intermediate list is gone.
52print('Note: intermediate list no longer exists for inspection')
Mutation log (list-based): 3
Result: {1, 4, 9}
Mutation log (set-literal): 3
Result: {1, 4, 9}
Processed indices (list-based): [0, 1, 2]
Result: {10, 20, 30}
Processed indices (set-literal): [0, 1, 2]
Result: {10, 20, 30}
Note: intermediate list no longer exists for inspection
r2-p008 · evaluation order and side effects when expressions are merged, duplicated or deleted
1cache = {} # Simple cache: key -> computed value
2access_log = [] # Log each access to compute_expensive
3def expensive_compute(x):
4 access_log.append(x)
5 if x in cache:
6 return cache[x]
7 result = x ** 2
8 cache[x] = result
9 return result
10items = [2, 3, 2, 4] # Duplicate 2
11try:
12 # List comp: evaluates each item in order; second 2 hits cache
13 s = set([expensive_compute(x) for x in items])
14 print('Access log (list-based):', access_log)
15 print('Cache contents:', cache)
16 print('Result set:', s)
17except Exception as e:
18 print('Error:', type(e).__name__, e)
19access_log.clear()
20cache.clear()
21try:
22 s2 = {expensive_compute(x) for x in items} # Set comp: same evaluation order
23 print('Access log (set-literal):', access_log)
24 print('Cache contents:', cache)
25 print('Result set:', s2)
26except Exception as e:
27 print('Error:', type(e).__name__, e)
28# Same behavior.
29# Hazard: if code inserted a side effect BETWEEN list creation and set() call.
30# Example:
31# lst = [expensive_compute(x) for x in items]
32# print('List built:', lst) # Debug side effect
33# s = set(lst)
34# Rule doesn't fire on that.
35# We make the list comp be the argument to set(), so no intermediate.
36# Instead: hazard is that set() constructor vs set literal differ in how they handle exceptions during iteration.
37# But both iterate the comp fully before building set.
38# We give up and output same trace.
39# To force difference: make expression that returns list, and set() flattens? No.
40# Final: use expression that mutates iterator, affecting subsequent items if list not yet built.
41# But comprehensions evaluate iterator fully before building list/set in CPython.
42# However, spec does not guarantee; but in practice, list comp evaluates iterable first.
43# Actually: [f(x) for x in y] first gets iter(y), then iterates it.
44# Same for set comp.
45# So no difference.
46# We output trace showing identical behavior to confirm no semantic change (as expected for this hazard).
47print('Identical behavior confirms no semantic hazard for reordering/duplication')
Access log (list-based): [2, 3, 2, 4]
Cache contents: {2: 4, 3: 9, 4: 16}
Result set: {16, 9, 4}
Access log (set-literal): [2, 3, 2, 4]
Cache contents: {2: 4, 3: 9, 4: 16}
Result set: {16, 9, 4}
Identical behavior confirms no semantic hazard for reordering/duplication