ruff C402
labelled unknown
Analysis Nemotron 3 Super
Flags uses of dict() wrapping a generator expression where key-value pairs are produced via tuple literals.
Fix: Rewrites dict((key, value) for ...) into {key: value for ...} by removing the dict() call and replacing the generator expression with a dict comprehension using the same key and value expressions.
10 hazards
- If the generator expression contains a yield statement inside a nested function, rewriting to a dict comprehension may alter generator behavior and break code relying on lazy evaluation.
- If the generator expression is used in a context where its object identity matters (e.g., passed to another function or stored), replacing it with a dict comprehension changes the object type and breaks identity checks.
- If the key or value expression contains side effects (e.g., function calls with mutable state), changing from generator to dict comprehension alters evaluation order or timing, potentially causing different behavior.
- If the generator expression includes asynchronous syntax (async for), the fix is invalid because dict comprehensions do not support async iteration, leading to a SyntaxError.
- If the generator expression is part of a larger expression where operator precedence affects parsing (e.g., dict((k,v) for k,v in items) + 1), removing dict() may change how the expression is grouped.
- If the generator expression defines a variable that is used later in the same scope (e.g., dict((k, v) for k, v in items); print(k)), the dict comprehension does not leak the loop variable, breaking downstream code.
- If the generator expression is empty and relies on the dict() constructor to produce an empty dict, the fix still works but may interfere with debugging or introspection tools that distinguish between constructor calls and literals.
- If the code contains a comment inside the generator expression (e.g., dict((k, # comment v) for k, v in items)), the autofix may drop or misplace it due to AST transformation limitations.
- If the generator expression uses a tuple with more than two elements (e.g., dict((a,b,c) for a,b,c in data)), the fix incorrectly assumes pair structure and may produce invalid syntax or runtime errors.
- If the generator expression is nested inside a lambda or conditional expression where the dict() call is necessary for correct grouping, removing it alters the meaning of the expression.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | invalid | Object Identity Check | If the generator expression is used in a context where its object identity matters (e.g., passed to another function or stored), replacing it with a dict comprehension changes the object type and brea |
| r1-p002 | fired, no fix | Side Effect Order Change | If the key or value expression contains side effects (e.g., function calls with mutable state), changing from generator to dict comprehension alters evaluation order or timing, potentially causing dif |
| r1-p003 | fired, no fix prediction off | Async For Incompatibility | If the generator expression includes asynchronous syntax (async for), the fix is invalid because dict comprehensions do not support async iteration, leading to a SyntaxError. |
| r1-p004 | silent prediction off | Evaluation Order Merge/Duplicate | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r1-p005 | fired, no fix | Loop Variable Leak | If the generator expression defines a variable that is used later in the same scope (e.g., dict((k, v) for k, v in items); print(k)), the dict comprehension does not leak the loop variable, breaking d |
| r1-p006 | fired, no fix | Comment Inside Generator | If the code contains a comment inside the generator expression (e.g., dict((k, # comment\nv) for k, v in items)), the autofix may drop or misplace it due to AST transformation limitations. |
| r1-p007 | fired, no fix | Truthiness and Identity Checks | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r1-p008 | fired, no fix | Exception Handling and Finally | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p001 | fired, no fix | Side Effects in Value Expression | Evaluation Order Merge/Duplicate - Side effects in value expression alter execution count/timing when dict() is replaced with dict comprehension |
| r2-p002 | fired, no fix | Operator Precedence Change | Evaluation Order Merge/Duplicate - Removing dict() changes grouping with + operator due to precedence |
| r2-p003 | silent prediction off | Default Argument Evaluation Once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once - Default arg captures generator at def-time, not call-time |
| r2-p004 | silent prediction off | Generator Consumed Once in Loop | generators vs lists, iterators consumed once, lazy vs eager evaluation - Generator expression exhausted after first use |
| r2-p005 | silent prediction off | Empty Generator Debug Distinction | If the generator expression is empty and relies on the dict() constructor to produce an empty dict, the fix still works but may interfere with debugging or introspection tools that distinguish between |
| r2-p006 | fired, no fix prediction off | Runtime Type Annotation | type annotations evaluated at runtime, typing-only imports - Annotation uses the dict() call in a forward reference or string annotation that breaks if rewritten |
| r2-p007 | fired, no fix | Path Construction with Relative Paths | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths - dict() call used in path construction where intermediate values matter |
| r2-p008 | fired, no fix | Float Rounding and Coercion | int/float/bool coercion, Decimal, division and rounding - Floating point accumulation in value expression behaves differently if evaluated multiple times due to merge/duplicate |
Sources
r1-p001 · Object Identity Check (generated program is not deterministic)
1def check_id(obj):
2 return id(obj)
3gen = ((k, v) for k, v in [(1, 'a'), (2, 'b')])
4d = dict(gen)
5print('dict id:', check_id(d))
6print('type:', type(d))
7print('generator consumed?', gen.__length_hint__())
r1-p002 · Side Effect Order Change
1state = []
2def f():
3 state.append(len(state))
4 return len(state)
5d = dict((f(), f()) for _ in range(2))
6print('side effects:', state)
7print('dict:', d)
side effects: [0, 1, 2, 3]
dict: {1: 2, 3: 4}r1-p003 · Async For Incompatibility
1import asyncio
2async def gen():
3 for i in range(2):
4 yield (i, i*2)
5async def main():
6 try:
7 # This line triggers C402 but fix is invalid
8 d = dict((k, v) for k, v in [(1,2),(3,4)])
9 print('dict:', d)
10 except Exception as e:
11 print(type(e).__name__, str(e))
12asyncio.run(main())
dict: {1: 2, 3: 4}r1-p004 · Evaluation Order Merge/Duplicate
1refs = []
2def make_pair(i):
3 refs.append(i)
4 return (i, i*10)
5d = dict(make_pair(i) for i in [1,2] if (refs.append(i+100) or True))
6print('refs:', refs)
7print('dict:', d)
refs: [101, 1, 102, 2]
dict: {1: 10, 2: 20}r1-p005 · Loop Variable Leak
1items = [(1, 'a'), (2, 'b')]
2d = dict((k, v) for k, v in items)
3try:
4 print('k after:', k)
5except NameError as e:
6 print('NameError:', type(e).__name__, str(e))
NameError: NameError name 'k' is not defined
r1-p006 · Comment Inside Generator
1d = dict((k, # comment
2 v) for k, v in [(1,2),(3,4)])
3print('dict:', d)
4# Note: Comment position may affect readability but not semantics
dict: {1: 2, 3: 4}r1-p007 · Truthiness and Identity Checks
1class Obj:
2 def __init__(self, v):
3 self.v = v
4 def __bool__(self):
5 return self.v % 2 == 0
6 def __eq__(self, other):
7 return isinstance(other, Obj) and self.v == other.v
8data = [(Obj(1), Obj(2)), (Obj(3), Obj(4))]
9d = dict((k, v) for k, v in data)
10for k, v in d.items():
11 print('key bool:', bool(k), 'value bool:', bool(v))
12 print('k is Obj(1)?', k is Obj(1))
13 print('k == Obj(1)?', k == Obj(1))
r1-p008 · Exception Handling and Finally
1log = []
2def risky(i):
3 if i == 1:
4 raise ValueError('bad')
5 log.append(i)
6 return (i, i*10)
7try:
8 d = dict((risky(i), i) for i in [0,1,2])
9except ValueError as e:
10 print('Caught:', type(e).__name__, str(e))
11finally:
12 print('log:', log)
Caught: ValueError bad log: [0]
r2-p001 · Side Effects in Value Expression
1counter = [0]
2def inc():
3 counter[0] += 1
4 return counter[0]
5d = dict((i, inc()) for i in range(3))
6print('dict result:', d)
7print('counter:', counter[0])
8counter[0] = 0
9d2 = {i: inc() for i in range(3)}
10print('comp result:', d2)
11print('counter after comp:', counter[0])
dict result: {0: 1, 1: 2, 2: 3}
counter: 3
comp result: {0: 1, 1: 2, 2: 3}
counter after comp: 3r2-p002 · Operator Precedence Change
1items = [(1, 'a'), (2, 'b')]
2expr = dict((k, v) for k, v in items) + [{'extra': 1}]
3print('expr type:', type(expr))
4print('expr length:', len(expr))
5try:
6 expr2 = {(k, v) for k, v in items} + [{'extra': 1}]
7 print('comp expr type:', type(expr2))
8 print('comp expr length:', len(expr2))
9except TypeError as e:
10 print('TypeError:', type(e).__name__, ':', str(e))
r2-p003 · Default Argument Evaluation Once
1def make_dict(seq=[(i, i*2) for i in range(3)]):
2 return dict(seq)
3def make_dict_fixed():
4 return {(k, v) for k, v in [(i, i*2) for i in range(3)]}
5print('make_dict():', make_dict())
6print('make_dict_fixed():', make_dict_fixed())
7# Now rebind the default
8seq = [(0, 0), (1, 1), (2, 2)]
9print('make_dict() after rebind:', make_dict())
make_dict(): {0: 0, 1: 2, 2: 4}
make_dict_fixed(): {(2, 4), (1, 2), (0, 0)}
make_dict() after rebind: {0: 0, 1: 2, 2: 4}r2-p004 · Generator Consumed Once in Loop
1pairs = ((i, i+1) for i in range(3))
2d1 = dict(pairs)
3print('d1:', d1)
4try:
5 d2 = dict(pairs) # pairs is exhausted
6 print('d2:', d2)
7except Exception as e:
8 print('Error on second dict:', type(e).__name__, ':', str(e))
9pairs = ((i, i+1) for i in range(3))
10dc = {k: v for k, v in pairs}
11print('dc:', dc)
12try:
13 dc2 = {k: v for k, v in pairs} # pairs exhausted
14 print('dc2:', dc2)
15except Exception as e:
16 print('Error on second comp:', type(e).__name__, ':', str(e))
d1: {0: 1, 1: 2, 2: 3}
d2: {}
dc: {0: 1, 1: 2, 2: 3}
dc2: {}r2-p005 · Empty Generator Debug Distinction
1empty_gen = (x for x in [] if False)
2d_empty = dict(empty_gen)
3print('dict from empty gen:', d_empty)
4print('type:', type(d_empty))
5print('isinstance dict:', isinstance(d_empty, dict))
6dc_empty = {k: v for k, v in empty_gen}
7print('dict comp from empty gen:', dc_empty)
8print('type:', type(dc_empty))
9print('isinstance dict:', isinstance(dc_empty, dict))
10# Simulate introspection: check call stack or source? Not possible without inspect.
11# Instead, show they are equal but constructor vs literal may matter in profiling/tracing.
12print('equal:', d_empty == dc_empty)
dict from empty gen: {}
type: <class 'dict'>
isinstance dict: True
dict comp from empty gen: {}
type: <class 'dict'>
isinstance dict: True
equal: Truer2-p006 · Runtime Type Annotation
1from __future__ import annotations
2T = dict((str, int) for _ in [()])
3print('T:', T)
4print('type of T:', type(T))
5# This doesn't fire rule because it's not dict((k,v) for...) but dict((type, type)...)
6# So expect_trigger=false. We show it's silent.
T: {<class 'str'>: <class 'int'>}
type of T: <class 'dict'>r2-p007 · Path Construction with Relative Paths
1import os
2paths = [('a', 'b.txt'), ('c', 'd.txt')]
3d_paths = dict((os.path.join(a, b), f'{a}_{b}') for a, b in paths)
4print('d_paths:', d_paths)
5# Now simulate missing intermediate dir? Not creating files.
6# Just show the values.
7for k, v in d_paths.items():
8 print(f'path: {k!r} -> {v!r}')
9dc_paths = {os.path.join(a, b): f'{a}_{b}' for a, b in paths}
10print('dc_paths:', dc_paths)
11for k, v in dc_paths.items():
12 print(f'path: {k!r} -> {v!r}')
d_paths: {'a/b.txt': 'a_b.txt', 'c/d.txt': 'c_d.txt'}
path: 'a/b.txt' -> 'a_b.txt'
path: 'c/d.txt' -> 'c_d.txt'
dc_paths: {'a/b.txt': 'a_b.txt', 'c/d.txt': 'c_d.txt'}
path: 'a/b.txt' -> 'a_b.txt'
path: 'c/d.txt' -> 'c_d.txt'r2-p008 · Float Rounding and Coercion
1values = [0.1, 0.2, 0.3]
2total = [0.0]
3def add_and_return(x):
4 total[0] += x
5 return total[0]
6# This will fire rule: dict((x, add_and_return(x)) for x in values)
7d_acc = dict((x, add_and_return(x)) for x in values)
8print('d_acc:', d_acc)
9print('final total:', total[0])
10total[0] = 0.0
11dc_acc = {x: add_and_return(x) for x in values}
12print('dc_acc:', dc_acc)
13print('final total after comp:', total[0])
d_acc: {0.1: 0.1, 0.2: 0.30000000000000004, 0.3: 0.6000000000000001}
final total: 0.6000000000000001
dc_acc: {0.1: 0.1, 0.2: 0.30000000000000004, 0.3: 0.6000000000000001}
final total after comp: 0.6000000000000001