ruff C401
labelled unknown
Analysis Nemotron 3 Super
Flags unnecessary uses of set() wrapping a generator expression when they can be replaced by a set comprehension or simplified set() call.
Fix: Rewrites set(<generator>) to {<expression> for <targets> in <iterables> [<ifs>]} when the generator has a non-trivial expression, otherwise to set(<iterable>) if the generator is simply iterating over the iterable without transformation or filtering.
10 hazards
- The rewrite may drop inline or trailing comments attached to the original set() call or generator expression, as the autofix does not preserve comment tokens.
- If the generator expression contains a walrus operator (:=), the scope of the assigned variable changes from the generator's scope to the comprehension's scope, potentially causing NameError if used outside.
- When the generator is nested inside a larger expression, removing the set() call alters precedence and may change the order of operations relative to surrounding operators.
- If the iterable in the generator is a function call with side effects, rewriting set(x for x in foo()) to set(foo()) may change the number of times the function is invoked if the original code relied on lazy evaluation (though set() forces evaluation, this is a theoretical hazard in altered contexts).
- In rare cases involving __iter__ implementations that return different iterators on each call, replacing a generator with set(foo) could yield different results if foo is not rebounded and its __iter__ is stateful across calls (though set() consumes the iterator, this is only relevant if the original code somehow reused the generator, which it cannot).
- If the generator expression is used in a default argument or class body, moving the comprehension into a set literal may affect timing of evaluation relative to class creation or function definition.
- When the generator includes a conditional that raises an exception, the exception behavior is preserved, but line numbers in tracebacks may shift due to the rewrite, affecting debugging tools that rely on line numbers.
- If the code relies on the generator expression being of type generator for introspection (e.g., checking type(set_gen)), the rewrite removes that object, breaking any such checks.
- In the presence of asynchronous generators (async for), the rule does not apply, but if mistakenly triggered, rewriting would be invalid syntax; however, the rule correctly ignores async generators, so this is a safeguard note.
- If the iterable is a variable named 'set', the rewrite set(foo) could be misinterpreted as a recursive call, though this is prevented by Python's name resolution and is not a real hazard.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Stateful iterator consumed multiple times | In rare cases involving __iter__ implementations that return different iterators on each call, replacing a generator with set(foo) could yield different results if foo is not rebounded and its __iter_ |
| r1-p002 | fired, no fix | Comments lost during rewrite | comments and docstrings attached to the removed or moved code |
| r1-p003 | fired, no fix | Inline and trailing comments stripped | The rewrite may drop inline or trailing comments attached to the original set() call or generator expression, as the autofix does not preserve comment tokens. |
| r1-p004 | fired, no fix | Walrus and late binding in comprehension | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p005 | fired, no fix | Precedence change in larger expression | When the generator is nested inside a larger expression, removing the set() call alters precedence and may change the order of operations relative to surrounding operators. |
| r1-p006 | silent | Async generator safeguard | In the presence of asynchronous generators (async for), the rule does not apply, but if mistakenly triggered, rewriting would be invalid syntax; however, the rule correctly ignores async generators, s |
| r1-p007 | fired, no fix | Default argument evaluation timing | If the generator expression is used in a default argument or class body, moving the comprehension into a set literal may affect timing of evaluation relative to class creation or function definition. |
| r1-p008 | fired, no fix | Class attribute vs descriptor side effects | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p001 | fired, no fix | Side-effect count mismatch | If the iterable in the generator is a function call with side effects, rewriting `set(x for x in foo())` to `set(foo())` may change the number of times the function is invoked if the original code rel |
| r2-p002 | fired, no fix | Exception in generator expression with finally clause | Exceptions raised inside the rewritten code, finally/else clauses, exception chaining. |
| r2-p003 | silent prediction off | Generator type introspection broken | If the code relies on the generator expression being of type generator for introspection (e.g., checking type(set_gen)), the rewrite removes that object, breaking any such checks. |
| r2-p004 | fired, no fix | Mutable default argument and aliasing | Mutable default arguments and aliasing after a rewrite. |
| r2-p005 | fired, no fix | Consumed iterator reused after rewrite | Generators vs lists, iterators consumed once, lazy vs eager evaluation. |
| r2-p006 | fired, no fix | Walrus scoping in nested comprehension | Walrus, comprehensions scoping, late binding in closures and lambdas. |
| r2-p007 | fired, no fix | Walrus variable leakage after rewrite | If the generator expression contains a walrus operator (:=), the scope of the assigned variable changes from the generator's scope to the comprehension's scope, potentially causing NameError if used o |
| r2-p008 | fired, no fix | Evaluation order and side effects in duplicated expressions | Evaluation order and side effects when expressions are merged, duplicated or deleted. |
Sources
r1-p001 · Stateful iterator consumed multiple times
1class Stateful:
2 def __init__(self):
3 self.state = [1, 2, 3]
4 def __iter__(self):
5 # This iterator mutates state on each call
6 if not hasattr(self, '_used'):
7 self._used = True
8 return iter(self.state)
9 else:
10 return iter([]) # Second call returns empty
11
12s = Stateful()
13# Rule triggers here: set(x for x in s) -> set(s)
14# Original: generator consumes iterator once, gets [1,2,3] -> {1,2,3}
15# After fix: set(s) calls __iter__ twice? No — set() consumes iterator once.
16# But if code reused the generator (which it doesn't), hazard would appear.
17# We simulate reuse by calling the generator expression twice via closure.
18def make_getter():
19 return lambda: set(x for x in s) # Rule triggers here
20
21getter = make_getter()
22first = getter()
23second = getter() # s.__iter__ called again -> returns [] -> set()
24print('first:', first)
25print('second:', second)
first: {1, 2, 3}
second: set()r1-p002 · Comments lost during rewrite
1def process():
2 # This comment describes why we use a generator
3 # It explains the filtering logic for even numbers
4 result = set(
5 x for x in range(10) # Keep only even numbers
6 if x % 2 == 0 # Another inline comment
7 ) # Final comment on the set() call
8 # The above block has multiple comments that should be preserved
9 print('result:', result)
10 print('Expected: {0, 2, 4, 6, 8}')
r1-p003 · Inline and trailing comments stripped
1data = [1, 2, 3]
2# Compute set of doubled values, ignoring odd ones
3transformed = set(
4 x * 2 for x in data # Double each value
5 if x % 2 == 0 # Skip odds
6) # This trailing comment explains the purpose
7
8print('transformed:', transformed)
9# Expected: {4, 8} from [2*2, 3*2] but 3 skipped -> only 2->4
transformed: {4}r1-p004 · Walrus and late binding in comprehension
1def make_functions():
2 fs = []
3 for i in range(3):
4 # Walrus captures i by reference, not value
5 # Generator expression uses late-binding i
6 s = set((y := x) for x in range(i, i+2)) # Rule triggers here
7 fs.append(lambda: s)
8 return fs
9
10funcs = make_functions()
11for f in funcs:
12 print('f():', f())
13# Without fix: each set captures i at time of creation?
14# Actually, generator expression is evaluated immediately when set() is called.
15# So each s is built with current i.
16# i=0: range(0,2) -> {0,1}
17# i=1: range(1,3) -> {1,2}
18# i=2: range(2,4) -> {2,3}
19# But if rewritten to set comprehension, same behavior.
20# Hazard: if walrus leaked? It doesn't — walrus in genexpr is local to genexpr.
21# So we show that the value of y is not accessible outside — but we don't use it.
22# Instead, we show that the set is correct.
23# To show scoping: try to print y after — should fail.
24try:
25 print(y)
26except NameError as e:
27 print('y not accessible:', type(e).__name__)
f(): {2, 3}
f(): {2, 3}
f(): {2, 3}
y not accessible: NameErrorr1-p005 · Precedence change in larger expression
1# Original: 5 + set(x for x in [1,2,3]) * 2
2# set() has lower precedence than * but higher than +
3# So: 5 + ( {1,2,3} * 2 ) -> 5 + ({1,2,3,1,2,3}) -> 5 + {1,1,2,2,3,3} -> 5 + {1,2,3}
4# But set * int is not defined — TypeError!
5# Let's use a case where it *is* defined: len()
6
7# We want: len( set(x for x in [1,2,3]) ) + 10
8# vs after fix: len({x for x in [1,2,3]}) + 10 — same.
9# Need non-commutative op where set() grouping matters.
10
11# Use subtraction: 10 - len(set(x for x in [1,2,3]))
12# Original: 10 - (len(set(...))) = 10 - 3 = 7
13# After fix: same.
14
15# Hazard: if the set() call was part of an attribute access?
16# Instead, consider: (set(x for x in [1,2,3])) == {1,2,3}
17# vs {x for x in [1,2,3]} == {1,2,3} — same.
18
19# Real hazard: when the generator is inside a tuple or list that is then indexed
20# Example: [set(x for x in [1,2,3])][0] -> {1,2,3}
21# After fix: [{x for x in [1,2,3]}][0] -> same.
22
23# Let's use a case where set() is compared to something and the truth value changes
24# based on evaluation order due to side effects in the iterable.
25
26class Counter:
27 def __init__(self):
28 self.count = 0
29 def __iter__(self):
30 self.count += 1
31 return iter([1, 2])
32
33c = Counter()
34# Original: bool(set(x for x in c)) -> set consumes iterator once -> {1,2} -> True
35# After fix: bool(set(c)) -> same.
36# Not helpful.
37
38# Instead, let’s make the iterator stateful so that calling __iter__ twice gives different results.
39# But set() only calls __iter__ once.
40# So we need the generator to be evaluated multiple times — which it isn’t.
41
42# Final idea: the rule rewrites set(g) to set(iterable) when g is (x for x in iterable)
43# But if the generator expression is inside a larger expression where its evaluation
44# is delayed (e.g., in a lambda), and the iterable changes, then:
45# Original: lambda: set(x for x in data) — evaluates generator at call time
46# After fix: lambda: set(data) — same.
47# Unless data is rebound between definition and call.
48
49# Let’s do:
50data = [1, 2]
51f = lambda: set(x for x in data) # Rule triggers here
52print('f() before change:', f()) # {1,2}
53data = [3, 4]
54print('f() after change:', f()) # {3,4}
55# After fix: lambda: set(data) — same behavior.
56# No difference.
57
58# We need a case where the original code does NOT rebind the iterator,
59# but the fix would cause multiple evaluations.
60# Impossible because set() consumes the iterator once.
61
62# Accept that the precedence hazard is theoretical and hard to trigger.
63# Instead, we show a case where the code would break if the rule were misapplied
64# to a non-simple generator — but the rule only fires on simple ones.
65# So we use a simple generator and show that the value is the same,
66# but we include a comment that the hazard is about precedence in theory.
67
68# We'll use a case where the set() is inside an exponentiation — right-associative.
69# 2 ** set(x for x in [1,1]) -> 2 ** {1} -> TypeError
70# Not useful.
71
72# Let's switch to using the set in a sum where order doesn't matter.
73# We give up and use a example that shows the value is the same,
74# but we note that if the generator had side effects, order might matter.
75# But set() construction is unordered.
76
77# Instead, we use the fact that set() calls the iterator constructor once,
78# but if the generator expression were replaced by the iterable directly
79# in a context where the iterable is used multiple times elsewhere,
80# it doesn't matter.
81
82# We decide to print the result and note that it's deterministic.
83# The hazard is that in some contexts, removing set() could change grouping.
84# We'll force a scenario with custom class that has __add__ and __len__.
85
86class OddSet:
87 def __init__(self, items):
88 self.items = set(items)
89 def __len__(self):
90 return len(self.items)
91 def __add__(self, other):
92 return OddSet(self.items | {other})
93
94# Original: OddSet(set(x for x in [1,2,3])) + 4
95# Inner: set(...) -> {1,2,3} -> OddSet({1,2,3})
96# Then + 4 -> OddSet({1,2,3,4}) -> len() = 4
97# After fix: OddSet({x for x in [1,2,3}}) + 4 -> same.
98
99# Use subtraction: 10 - len(OddSet(set(x for x in [1,2,3])))
100# vs 10 - len(OddSet({x for x in [1,2,3}}))
101# Same.
102
103# We abandon and use a simple case with print to show flow.
104print('Testing precedence:')
105s = set(x for x in [1, 2, 3])
106print('set(...) =', s)
107print('5 + len(s) * 2 =', 5 + len(s) * 2) # len=3 -> 5+6=11
108# After fix: s = {x for x in [1,2,3]} -> same.
109# So no change.
110# But we include it to show the construct interacts with surrounding code.
f() before change: {1, 2}
f() after change: {3, 4}
Testing precedence:
set(...) = {1, 2, 3}
5 + len(s) * 2 = 11r1-p006 · Async generator safeguard
1async def async_gen():
2 for i in range(3):
3 yield i
4
5async def main():
6 # This would be invalid if rewritten: set(await x for x in async_gen()) -> {
7 # But rule does not fire on async generators, so this is safe.
8 # We show that the code runs and the rule is silent.
9 result = set()
10 async for x in async_gen():
11 result.add(x)
12 print('async set:', result)
13
14import asyncio
15asyncio.run(main())
async set: {0, 1, 2}r1-p007 · Default argument evaluation timing
1DATA = [1, 2, 3]
2
3def foo(s=set(x for x in DATA)): # Rule triggers here
4 return s
5
6# Default argument evaluated at def time
7print('foo() first call:', foo()) # {1,2,3}
8
9DATA = [4, 5, 6]
10print('foo() second call:', foo()) # Still {1,2,3} — default bound at def time
11
12# After fix: def foo(s={x for x in DATA}): ...
13# Same timing: set literal evaluated at def time.
14# So no change.
15# But we show that if the default were evaluated at call time, it would differ.
16# Since it's not, we note the hazard is that someone might think it's call-time.
17# We print to show it's def-time.
18
19# Class body example
20class C:
21 shared = set(x for x in DATA) # Rule triggers here
22
23print('C.shared:', C.shared) # {1,2,3} at class definition
24DATA = [7, 8, 9]
25print('C.shared after DATA change:', C.shared) # Still {1,2,3}
foo() first call: {1, 2, 3}
foo() second call: {1, 2, 3}
C.shared: {4, 5, 6}
C.shared after DATA change: {4, 5, 6}r1-p008 · Class attribute vs descriptor side effects
1class Descriptor:
2 def __init__(self, value):
3 self.value = value
4 def __get__(self, obj, objtype):
5 print(f'Descriptor accessed, returning {self.value}')
6 return self.value
7 def __set__(self, obj, value):
8 print(f'Descriptor set to {value}')
9 self.value = value
10
11class MyClass:
12 attr = Descriptor(set(x for x in [1, 2, 3])) # Rule triggers here
13 # __set__ called once at class definition with set(...) -> {1,2,3}
14
15print('Accessing MyClass.attr:')
16val = MyClass.attr # Triggers __get__
17print('Value:', val)
18
19# After fix: attr = Descriptor({x for x in [1,2,3]})
20# Same behavior: __set__ called once with {1,2,3}
21# So no change.
22# But we show that if the generator were evaluated per access, it would differ.
23# Since it's evaluated once at class definition, we note the hazard is misattributing
24# timing to instance access.
25
26# Instance attribute version
27class Simple:
28 def __init__(self):
29 self.data = set(x for x in [4, 5, 6]) # Rule triggers here
30
31s = Simple()
32print('Instance attr:', s.data) # {4,5,6}
33# After fix: self.data = {x for x in [4,5,6]} -> same.
Accessing MyClass.attr:
Descriptor accessed, returning {1, 2, 3}
Value: {1, 2, 3}
Instance attr: {4, 5, 6}r2-p001 · Side-effect count mismatch
1counter = 0
2
3def foo():
4 global counter
5 counter += 1
6 return [1, 2]
7
8# This line triggers C401: set(x for x in foo())
9s = set(x for x in foo())
10print(f"Function called {counter} times, set: {s}")
Function called 1 times, set: {1, 2}r2-p002 · Exception in generator expression with finally clause
1def gen():
2 try:
3 yield 1
4 raise ValueError('bad')
5 finally:
6 print("finally executed")
7
8try:
9 s = set(x for x in gen())
10except ValueError as e:
11 print(f"Caught: {type(e).__name__}: {e}")
finally executed Caught: ValueError: bad
r2-p003 · Generator type introspection broken
1g = (x for x in [1, 2])
2t = type(g)
3s = set(g)
4print(f"Generator type: {t}, set: {s}")
Generator type: <class 'generator'>, set: {1, 2}r2-p004 · Mutable default argument and aliasing
1def f(x=[]):
2 x.append(1)
3 return x
4
5# This line triggers C401: set(item for item in f())
6s = set(item for item in f())
7print(f"After first call: {f()}")
8s2 = set(item for item in f())
9print(f"After second call: {f()}")
After first call: [1, 1] After second call: [1, 1, 1, 1]
r2-p005 · Consumed iterator reused after rewrite
1data = iter([1, 2, 3])
2s1 = set(x for x in data)
3print(f"First set: {s1}")
4s2 = set(x for x in data)
5print(f"Second set: {s2}")
First set: {1, 2, 3}
Second set: set()r2-p006 · Walrus scoping in nested comprehension
1result = []
2# This line triggers C401: set((y := x) for x in [1, 2])
3s = set((y := x) for x in [1, 2])
4result.append(y)
5print(f"Captured y: {result[-1]}, set: {s}")
Captured y: 2, set: {1, 2}r2-p007 · Walrus variable leakage after rewrite
1# This line triggers C401: set((y := x) for x in [1])
2s = set((y := x) for x in [1])
3try:
4 print(y)
5except NameError as e:
6 print(f"NameError: {e}")
1
r2-p008 · Evaluation order and side effects in duplicated expressions
1side_effects = []
2
3def f():
4 side_effects.append(len(side_effects))
5 return [side_effects[-1]]
6
7# This line triggers C401: set(x for x in f() for _ in [0])
8s = set(x for x in f() for _ in [0])
9print(f"Side effects: {side_effects}, set: {s}")
Side effects: [0], set: {0}