ruff C416
labelled unknown
Analysis Nemotron 3 Super
Flags dict, list, or set comprehensions where the output expression equals the iteration variable(s), suggesting replacement with direct constructor calls.
Fix: Replaces the comprehension with a call to dict(), list(), or set() using the same iterable expression, preserving only the iterable part and discarding any conditionals or transformed expressions.
12 hazards
- Rewriting `{a: b for a, b in mapping}` as `dict(mapping)` changes behavior when `mapping` is a dict, because the comprehension iterates over keys while `dict(mapping)` preserves key-value pairs.
- Rewriting `{x for x in seq}` as `set(seq)` may raise a TypeError if `seq` contains unhashable elements like lists or dicts, whereas the comprehension would fail earlier during iteration.
- Rewriting `[x for x in iterable]` as `list(iterable)` can trigger a RecursionError if `iterable` is an object whose `__iter__` method calls `len()` on the containing object, and the list constructor invokes `__len__`.
- Rewriting a comprehension inside a lambda or nested function may alter variable scoping if the iterable expression captures variables from an outer scope that are later rebound, due to differences in when the iterable is evaluated.
- If the iterable has side effects (e.g., a function call with prints or state changes), moving it into a constructor call may not change behavior, but if the comprehension was inside a larger expression, reordering could affect evaluation order relative to other side effects.
- Comments inside the comprehension brackets (e.g., `{x # comment for x in y}`) are lost during the rewrite, as the autofix does not preserve internal comment syntax.
- Rewriting `{k: v for k, v in zip(a, b)}` as `dict(zip(a, b))` is safe only if `zip(a, b)` produces pairs; however, if `a` and `b` are of unequal length, the behavior matches, but if the zip object is reused elsewhere, its state may be consumed earlier.
- Rewriting a comprehension used in a default argument (e.g., `def f(x={y: z for y, z in items}):`) may cause premature evaluation of the iterable at definition time instead of call time, changing when side effects occur.
- If the iterable is a class property or descriptor, accessing it via the constructor may trigger different behavior than in the comprehension due to how `__get__` is invoked.
- Rewriting `{x for x in obj}` as `set(obj)` may invoke `__iter__` on `obj` twice if the constructor internally checks for set-like behavior, potentially causing inconsistent iteration or side effects.
- In a boolean context like `if {x for x in items}:`, rewriting as `if set(items):` may behave differently if `items` is a non-list iterable that defines `__len__` to raise an exception, since `set()` may not call `__len__` while the comprehension does during iteration.
- If the comprehension includes a walrus operator (e.g., `{x: y for x, y in data if (tmp := x) is not None}`), the autofix does not apply, but if mistakenly used, removing the condition would break scoping of the assignment expression.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Zip consumption hazard | Rewriting {k: v for k, v in zip(a, b)} as dict(zip(a, b)) consumes the zip iterator early if reused. |
| r1-p002 | fired, no fix | Property descriptor access | Accessing a property via dict() constructor may invoke __get__ differently than in a comprehension. |
| r1-p003 | fired, no fix | Default argument evaluation timing | Rewriting comprehension in default arg causes premature evaluation at definition time. |
| r1-p004 | silent prediction off | Mutable default aliasing | Rewriting comprehension in default arg creates shared mutable default across calls. |
| r1-p005 | silent | Walrus operator scoping | If autofix incorrectly removes condition with walrus, breaks assignment expression scope. |
| r1-p006 | silent | Coercion and arithmetic | Rule silent when expression transforms values; no rewrite occurs. |
| r1-p007 | fired, no fix | Re-entry and loop interaction | Comprehension in loop interacts with rewritten form via side effects in iterable. |
| r1-p008 | fired, no fix | Class vs instance attribute | Attribute access via constructor may resolve to class attribute where comprehension uses instance. |
| r2-p001 | fired, no fix | Unhashable Elements in Set Comprehension | Rewriting `{x for x in seq}` as `set(seq)` may raise a TypeError if `seq` contains unhashable elements like lists or dicts, whereas the comprehension would fail earlier during iteration. |
| r2-p002 | fired, no fix | Default Argument Evaluation Timing | Rewriting a comprehension used in a default argument (e.g., `def f(x={y: z for y, z in items}):`) may cause premature evaluation of the iterable at definition time instead of call time, changing when |
| r2-p003 | invalid | Lost Comments in Comprehension | Comments inside the comprehension brackets (e.g., `{x # comment for x in y}`) are lost during the rewrite, as the autofix does not preserve internal comment syntax. |
| r2-p004 | fired, no fix | Bytes vs Str Encoding Handling | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p005 | fired, no fix | Lambda Scoping with Rebinding | Rewriting a comprehension inside a lambda or nested function may alter variable scoping if the iterable expression captures variables from an outer scope that are later rebound, due to differences in |
| r2-p006 | fired, no fix | Numeric Coercion and Rounding | int/float/bool coercion, Decimal, division and rounding |
| r2-p007 | fired, no fix | Keyword-only Arguments and Defaults | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p008 | fired, no fix | Double __iter__ Invocation | Rewriting `{x for x in obj}` as `set(obj)` may invoke `__iter__` on `obj` twice if the constructor internally checks for set-like behavior, potentially causing inconsistent iteration or side effects. |
Sources
r1-p001 · Zip consumption hazard
1a = [1, 2]
2b = [3, 4]
3z = zip(a, b)
4d1 = {k: v for k, v in z}
5print('len(d1):', len(d1))
6try:
7 d2 = {k: v for k, v in z}
8except ValueError as e:
9 print('second dict error:', type(e).__name__, str(e))
10else:
11 print('d2 len:', len(d2))
len(d1): 2 d2 len: 0
r1-p002 · Property descriptor access
1class C:
2 @property
3 def items(self):
4 print('items called')
5 return [('a', 1), ('b', 2)]
6c = C()
7print('before')
8d = {k: v for k, v in c.items}
9print('after comp:', d)
10c2 = C()
11print('before')
12d2 = dict(c2.items)
13print('after dict:', d2)
before
items called
after comp: {'a': 1, 'b': 2}
before
items called
after dict: {'a': 1, 'b': 2}r1-p003 · Default argument evaluation timing
1state = []
2def make_items():
3 state.append(1)
4 return [('x', 99)]
5def f(d={k: v for k, v in make_items()}):
6 return len(d)
7print('state after def:', state)
8print('f():', f())
9print('state after call:', state)
state after def: [1] f(): 1 state after call: [1]
r1-p004 · Mutable default aliasing
1def g(d={k: k for k in range(2)}):
2 d.append(99)
3 return d
4print('g():', g())
5print('g():', g())
r1-p005 · Walrus operator scoping
1data = [1, 2, 3]
2result = {x: y for x, y in [(x, x*2) for x in data] if (tmp := x) is not None}
3print('result:', result)
4try:
5 print('tmp:', tmp)
6except NameError as e:
7 print('tmp not accessible:', type(e).__name__)
result: {1: 2, 2: 4, 3: 6}
tmp: 3r1-p006 · Coercion and arithmetic
1items = [('a', 1), ('b', 2)]
2d = {k: v*2 for k, v in items}
3print('transformed:', d)
4l = [x+1 for x in [0, 1, 2]]
5print('incremented:', l)
6s = {x for x in [1, 2, 3] if x > 1}
7print('filtered set:', s)
transformed: {'a': 2, 'b': 4}
incremented: [1, 2, 3]
filtered set: {2, 3}r1-p007 · Re-entry and loop interaction
1counter = 0
2def gen():
3 global counter
4 counter += 1
5 yield (counter, counter*10)
6for i in range(2):
7 d = {k: v for k, v in gen()}
8 print('round', i, 'dict:', d, 'counter:', counter)
round 0 dict: {1: 10} counter: 1
round 1 dict: {2: 20} counter: 2r1-p008 · Class vs instance attribute
1class D:
2 attr = [('x', 1)]
3 @property
4 def items(self):
5 return self.attr if hasattr(self, 'attr') else [('y', 2)]
6d1 = D()
7print('comp:', {k: v for k, v in d1.items})
8d1.attr = [('z', 3)]
9print('comp after set:', {k: v for k, v in d1.items})
10d2 = D()
11print('dict constr:', dict(d2.items))
comp: {'x': 1}
comp after set: {'z': 3}
dict constr: {'x': 1}r2-p001 · Unhashable Elements in Set Comprehension
1data = [[1], [2]]
2try:
3 result = {x for x in data}
4except Exception as e:
5 print(f'comprehension error: {type(e).__name__}: {e}')
6else:
7 print(f'comprehension success: {result}')
8data = [[1], [2]]
9try:
10 result = set(data)
11except Exception as e:
12 print(f'set() error: {type(e).__name__}: {e}')
13else:
14 print(f'set() success: {result}')
comprehension error: TypeError: cannot use 'list' as a set element (unhashable type: 'list') set() error: TypeError: cannot use 'list' as a set element (unhashable type: 'list')
r2-p002 · Default Argument Evaluation Timing
1items = [(1, 'a'), (2, 'b')]
2def get_items():
3 print('get_items called')
4 return items
5# Rule fires here: rewrites {y: z for y, z in get_items()} to dict(get_items())
6def f(d={y: z for y, z in get_items()}):
7 print('f called')
8 return d
9print('Before first call:')
10r1 = f()
11items.append((3, 'c'))
12print('After modifying items:')
13r2 = f()
14print(f'First result: {r1}')
15print(f'Second result: {r2}')
get_items called
Before first call:
f called
After modifying items:
f called
First result: {1: 'a', 2: 'b'}
Second result: {1: 'a', 2: 'b'}r2-p003 · Lost Comments in Comprehension (generated program does not parse: Traceback (most recent call last):)
1# This line documents why we're filtering
2vals = [1, 2, 3]
3# Rule fires here: rewrites {x # keep value
4 for x in vals} to set(vals), losing the comment
5result = {x # keep value
6 for x in vals}
7print(f'Result: {result}')
8# After rewrite, the comment is gone but behavior should be same for hashable items
9try:
10 result2 = set(vals)
11 print(f'set(vals): {result2}')
12except Exception as e:
13 print(f'set(vals) error: {e}')
r2-p004 · Bytes vs Str Encoding Handling
1text = 'café\n'
2# Rule fires here: rewrites {c for c in text} to set(text)
3chars = {c for c in text}
4print(f'Chars from text: {sorted(chars)}')
5# For bytes, similar issue
6b = b'caf\xc3\xa9\n'
7# Rule fires here: rewrites {c for c in b} to set(b)
8byte_chars = {c for c in b}
9print(f'Byte values: {sorted(byte_chars)}')
10# Compare with direct set() calls
11print(f'set(text): {sorted(set(text))}')
12print(f'set(b): {sorted(set(b))}')
Chars from text: ['\n', 'a', 'c', 'f', 'é'] Byte values: [10, 97, 99, 102, 169, 195] set(text): ['\n', 'a', 'c', 'f', 'é'] set(b): [10, 97, 99, 102, 169, 195]
r2-p005 · Lambda Scoping with Rebinding
1x = [1, 2, 3]
2# Rule fires here: rewrites {i for i in x} to set(x) inside lambda
3func = lambda: {i for i in x}
4print(f'Before rebinding: {func()}')
5x = [4, 5, 6] # Rebinding x
6print(f'After rebinding: {func()}')
7# Now test with set(x) version - same behavior since set(x) also uses current x
8# But to show hazard, we need case where timing differs - use mutable default
9x = [1, 2, 3]
10def make_func():
11 return lambda: {i for i in x} # Comprehension version
12f1 = make_func()
13x = [4, 5, 6]
14print(f'Comprehension lambda after rebinding: {f1()}')
15# Reset for set() test
16x = [1, 2, 3]
17def make_func_set():
18 return lambda: set(x) # What rule would produce
19f2 = make_func_set()
20x = [4, 5, 6]
21print(f'set() lambda after rebinding: {f2()}')
Before rebinding: {1, 2, 3}
After rebinding: {4, 5, 6}
Comprehension lambda after rebinding: {4, 5, 6}
set() lambda after rebinding: {4, 5, 6}r2-p006 · Numeric Coercion and Rounding
1from decimal import Decimal
2values = [1.0, 2.0, 3.0]
3# Rule fires here: rewrites {int(v) for v in values} to set(map(int, values))? No - wait
4# Actually rule only fires when output equals iteration variable
5# So we need: {v for v in values} -> set(values)
6# Hazard: if values contains floats that are equal but different types?
7# Better: use Decimal and float that compare equal
8vals = [Decimal('1.0'), float(1.0), True]
9# Rule fires: {v for v in vals} -> set(vals)
10# In set(), Decimal('1.0') == float(1.0) == True -> all hash to same value?
11# Actually: Decimal and float are not equal, but True == 1
12result = {v for v in vals}
13print(f'Comprehension result: {result} (length: {len(result)})')
14result2 = set(vals)
15print(f'set() result: {result2} (length: {len(result2)})')
16# Show individual equality
17print(f'Decimal("1.0") == float(1.0): {Decimal("1.0") == float(1.0)}')
18print(f'float(1.0) == True: {float(1.0) == True}')
19print(f'Decimal("1.0") == True: {Decimal("1.0") == True}' )
Comprehension result: {Decimal('1.0')} (length: 1)
set() result: {Decimal('1.0')} (length: 1)
Decimal("1.0") == float(1.0): True
float(1.0) == True: True
Decimal("1.0") == True: Truer2-p007 · Keyword-only Arguments and Defaults
1items = [('a', 1)]
2# Rule fires here in default arg: rewrites {k: v for k, v in items} to dict(items)
3def process(*, mapping={k: v for k, v in items}):
4 print('process called')
5 return mapping.copy()
6print('First call:')
7r1 = process()
8items.append(('b', 2))
9print('Second call (after modifying items):')
10r2 = process()
11print(f'First result: {r1}')
12print(f'Second result: {r2}')
13print(f'Same object? {r1 is r2}')
First call:
process called
Second call (after modifying items):
process called
First result: {'a': 1}
Second result: {'a': 1}
Same object? Falser2-p008 · Double __iter__ Invocation
1class IterableObj:
2 def __init__(self, data):
3 self.data = data
4 self.iter_count = 0
5 def __iter__(self):
6 self.iter_count += 1
7 print(f'__iter__ called #{self.iter_count}')
8 return iter(self.data)
9obj = IterableObj([1, 2, 3])
10print('Testing comprehension:')
11obj.iter_count = 0
12result1 = {x for x in obj}
13print(f'Result: {result1}, __iter__ calls: {obj.iter_count}')
14obj.iter_count = 0
15print('Testing set() constructor:')
16result2 = set(obj)
17print(f'Result: {result2}, __iter__ calls: {obj.iter_count}')
18# To see if set() calls __iter__ more than once, we need to check if it does internal checks
19# Some implementations might check isinstance(obj, set) or similar, but set() constructor
20# typically just calls __iter__ once. However, let's make it call __iter__ twice by
21# having __iter__ return a different iterator each time and checking if we get same results
22class DoubleCheckIter:
23 def __init__(self, data):
24 self.data = data
25 self.call_count = 0
26 def __iter__(self):
27 self.call_count += 1
28 print(f'__iter__ call #{self.call_count}')
29 if self.call_count == 1:
30 return iter(self.data[:]) # First copy
31 else:
32 return iter(self.data[::-1]) # Reversed on second call
33obj2 = DoubleCheckIter([1, 2, 3])
34print('\nComprehension with inconsistent __iter__:')
35obj2.call_count = 0
36try:
37 result1 = {x for x in obj2}
38 print(f'Result: {sorted(result1)}')
39except Exception as e:
40 print(f'Error: {e}')
41print(f'__iter__ calls: {obj2.call_count}')
42obj2.call_count = 0
43print('\nset() with same inconsistent __iter__:')
44try:
45 result2 = set(obj2)
46 print(f'Result: {sorted(result2)}')
47except Exception as e:
48 print(f'Error: {e}')
49print(f'__iter__ calls: {obj2.call_count}')
Testing comprehension:
__iter__ called #1
Result: {1, 2, 3}, __iter__ calls: 1
Testing set() constructor:
__iter__ called #1
Result: {1, 2, 3}, __iter__ calls: 1
Comprehension with inconsistent __iter__:
__iter__ call #1
Result: [1, 2, 3]
__iter__ calls: 1
set() with same inconsistent __iter__:
__iter__ call #1
Result: [1, 2, 3]
__iter__ calls: 1