fixhunt

ruff C404

labelled unknown
Runs / 2026-10-17T20-19-11-157Z-cejr-ruff-C404ruff 0.17.12026-10-17 20:19 UTC8 programs × 2 round(s)16 programs, 12 fired, 0 fixed, 0 findings, 2 rejectedmodel cost $0.0053

Analysis Nemotron 3 Super

Flags when dict() wraps a list comprehension producing key-value pairs.

Fix: Rewrites dict([(expr1, expr2) for target in iterable if cond]) to {expr1: expr2 for target in iterable if cond}, preserving comprehension structure and filters.

10 hazards
  • If the list comprehension uses a variable name that shadows an outer variable in the dict key or value expression, the dict comprehension may capture that variable differently due to its own local scope.
  • If the iterable expression has side effects (e.g., a function call), moving it into the dict comprehension changes when those effects occur relative to other expressions in the same statement.
  • If the list comprehension is nested inside another expression where the list's identity or mutability matters (e.g., passed to a function that modifies it), replacing it with a dict comprehension alters the object type and breaks expectations.
  • If the comprehension contains a bare yield or yield-from, rewriting it as a dict comprehension changes it from a generator expression to a dict comprehension, altering execution timing and preventing lazy evaluation.
  • If the original code relies on the list being built eagerly (e.g., for truthiness or length before iteration), the dict comprehension's lazy-like evaluation (though still eager in CPython) may not preserve intermediate state assumptions in rare edge cases involving __bool__ or __len__ overrides.
  • If the list comprehension is part of a larger expression where operator precedence groups it unexpectedly (e.g., dict([(k, v) for k, v in items]) + {}), removing the list constructor could change parsing if parentheses are omitted incorrectly during rewrite.
  • If the comprehension includes a tuple with more than two elements (e.g., [(k, v, w) for ...]), the rule should not fire, but if misapplied, the fix would raise a ValueError due to too many values to unpack in the dict comprehension.
  • If the list comprehension is inside a lambda or nested function and uses a nonlocal variable, the dict comprehension's implicit scope may not preserve the same closure behavior, especially in Python 2 or under atypical execution contexts.
  • Comments attached to the list brackets or for/clause tokens may be dropped during AST rewrite, even if the tool attempts preservation, due to formatting loss in the conversion process.
  • If the iterable is the same variable being assigned to in the comprehension (e.g., dict([(x, x+1) for x in xs]) where xs is later reused), the rewrite is safe, but if the variable is deleted or reassigned mid-comprehension via del or side effect, the timing of observation differs.

Programs

idresulttitlehazard
r1-p001fired, no fixSide effects in iterable expressionIf the iterable expression has side effects (e.g., a function call), moving it into the dict comprehension changes when those effects occur relative to other expressions in the same statement.
r1-p002fired, no fixContext manager interactioncontext managers that are not files, objects with write() but no close()
r1-p003invalidBare yield in comprehensionIf the comprehension contains a bare yield or yield-from, rewriting it as a dict comprehension changes it from a generator expression to a dict comprehension, altering execution timing and preventing
r1-p004fired, no fixClass vs instance attributes, descriptorsclass attributes vs instance attributes, descriptors, properties with side effects
r1-p005fired, no fixEvaluation order and side effectsevaluation order and side effects when expressions are merged, duplicated or deleted
r1-p006fired, no fixString formatting edge casesstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r1-p007fired, no fixTruthiness and coerciontruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r1-p008fired, no fixNumeric coercion and roundingint/float/bool coercion, Decimal, division and rounding
r2-p001fired, no fixComment Loss HazardComments attached to list brackets or for-clauses may be dropped during AST-to-source rewrite.
r2-p002fired, no fixRebinding During ComprehensionRebinding the iterable variable mid-comprehension changes observation timing between list and dict forms.
r2-p003silent prediction offEagerness and TruthinessReliance on list truthiness/length during construction differs if __bool__ has side effects.
r2-p004invalidTruthiness and Identity Edge Cases__bool__/__len__ overrides, None vs falsy, 'is' vs '==' affect reliance on list vs dict intermediate state.
r2-p005fired, no fixMutability and Identity in ContextPassing the list to a function that mutates it; dict comp yields different type, breaking expectations.
r2-p006fired, no fixStar Imports and Module Side EffectsModule-level side effects from * imports or __all__ may be affected if the rewrite occurs at module scope.
r2-p007fired, no fixKeyword-Only and Defaults EvaluationDefaults in keyword-only args evaluated once; comprehension scope may interact if closure captures mutable default.
r2-p008silent prediction offPathlib vs os.path BehaviorBehavioral differences between pathlib and os.path for paths, symlinks, or bytes paths affected by rewrite timing.

Sources

r1-p001 · Side effects in iterable expression
1def make_list():
2 print('iterable')
3 return [1, 2]
4d = dict([(i, i*i) for i in make_list()])
5print(dict(d))
iterable
{1: 1, 2: 4}
r1-p002 · Context manager interaction
1class LogWriter:
2 def __enter__(self):
3 self.buf = []
4 print('enter')
5 return self
6 def __exit__(self, *exc):
7 print('exit:', self.buf)
8 def write(self, s):
9 self.buf.append(s)
10with LogWriter() as w:
11 d = dict([(str(i), w.write(str(i))) for i in range(2)])
12 w.write('done')
13print('after')
enter
exit: ['0', '1', 'done']
after
r1-p003 · Bare yield in comprehension (generated program does not parse: Traceback (most recent call last):)
1# This case does not trigger C404 because yield makes it a generator expression,
2# not a list comprehension wrapping tuples.
3gen = ((i, yield i) for i in [1, 2])
4print('gen created')
5try:
6 next(gen)
7except StopIteration as e:
8 print('StopIteration:', e.value)
r1-p004 · Class vs instance attributes, descriptors
1class A:
2 val = 10
3 @property
4 def prop(self):
5 print('prop accessed')
6 return self.val
7a = A()
8d = dict([(k, getattr(a, k)) for k in ['val', 'prop']])
9print(dict(d))
prop accessed
{'val': 10, 'prop': 10}
r1-p005 · Evaluation order and side effects
1counter = 0
2def inc():
3 global counter
4 counter += 1
5 return counter
6d = dict([(inc(), inc()) for _ in range(2)])
7print('counter:', counter, 'dict:', dict(d))
counter: 4 dict: {1: 2, 3: 4}
r1-p006 · String formatting edge cases
1class S:
2 def __repr__(self): return "repr"
3 def __str__(self): return "str"
4s = S()
5d = dict([(f'{s}', str(s)) for _ in [1]])
6print(repr(d[f'{s}']))
'str'
r1-p007 · Truthiness and coercion
1class Falsy:
2 def __len__(self): return 0
3 def __bool__(self): return False
4f = Falsy()
5d = dict([(f, 'yes') for _ in [f] if f])
6print('dict:', dict(d), 'len:', len(d))
dict: {} len: 0
r1-p008 · Numeric coercion and rounding
1from decimal import Decimal, ROUND_DOWN
2x = Decimal('3.5')
3y = Decimal('2')
4d = dict([(int(x // y), float(x / y)) for _ in [1]])
5print('dict:', dict(d))
dict: {1: 1.75}
r2-p001 · Comment Loss Hazard
1def f():
2 # This comment should stay
3 d = dict([
4 (x, x*2) # inline
5 for x in [1,2] # loop comment
6 ])
7 return len(d)
r2-p002 · Rebinding During Comprehension
1xs = [1, 2]
2d = dict([(x, xs.pop()) for x in xs])
3print(d)
{1: 2}
r2-p003 · Eagerness and Truthiness
1class ListLike:
2 def __init__(self):
3 self.data = []
4 self.called = False
5 def __iter__(self):
6 return iter(self.data)
7 def __bool__(self):
8 self.called = True
9 return bool(self.data)
10L = ListLike()
11L.data = [1, 2]
12# Force list build then check truthiness
13lst = [(x, x) for x in L]
14print('list bool during:', bool(lst))
15d = dict(lst)
16print('dict from list:', d)
17# Now dict comp: truthiness checked during build?
18L2 = ListLike()
19L2.data = [1, 2]
20d2 = dict((x, x) for x in L2)
21print('dict comp bool called:', L2.called)
list bool during: True
dict from list: {1: 1, 2: 2}
dict comp bool called: False
r2-p004 · Truthiness and Identity Edge Cases (generated program is not deterministic)
1class OddLen:
2 def __len__(self): return 1
3 def __bool__(self): return False
4lst = [(None, OddLen()) for _ in range(1)]
5print('list is false:', not lst)
6d = dict(lst)
7print('dict value is OddLen:', isinstance(d[None], OddLen))
8# Dict comp version
9L = [OddLen()]
10d2 = dict((None, x) for x in L)
11print('dict comp value:', d2[None])
r2-p005 · Mutability and Identity in Context
1def mutate_and_return_length(lst):
2 lst.append((99, 99))
3 return len(lst)
4items = [(1, 2), (3, 4)]
5lst_comp = [(x, y) for x, y in items]
6print('mutate list result:', mutate_and_return_length(lst_comp))
7print('final list:', lst_comp)
8# Now with dict comp equivalent - but we pass a list!
9# So we force a list build then convert? No - to test hazard we must pass the comp directly.
10# Rewrite: if rule fires, it becomes dict comp -> we can't pass dict to mutate_and_return_length
11# So we design: call site expects list, but rule changes it to dict comp -> type mismatch
12# But we can't have both. Instead: store the comp result, then pass.
13# Hazard: if the list's mutability was relied on after.
14def g():
15 r = [(x, x) for x in [1,2]]
16 r.append((3,3))
17 return len(r)
18print('list mutability test:', g())
19# If rule rewrote the comp to dict, this would fail - but it doesn't fire on assignment to r?
20# It does: dict([(x,x) for x in [1,2]]) is the pattern. But here we don't wrap in dict().
21# So make it:
22h = dict([(x, x) for x in [1,2]])
23print('h is:', type(h))
24# Now, if we had passed the list comp to a mutator, but we didn't. Let's force:
25items = [(1,2)]
26b = list((x, x) for x in items) # this is the list comp
27print('b before mutate:', b, type(b))
28mutate_and_return_length(b)
29print('b after mutate:', b)
30# Now the dict version that the rule would produce if we wrapped in dict():
31# But we don't. So to test the hazard, we need a case where the list comp is used as a list.
32# The hazard is: if someone passes the list comp (not wrapped) to a mutating function, and elsewhere
33# there is a dict([...]) that gets rewritten, they are unrelated.
34# So instead: make the list comp be the argument, and have a dict(...) nearby that gets rewritten.
35# But they are separate. Unless we do:
36# f(dict([(x,x) for x in xs])) and f expects a list? No.
37# We need the dict(...) to be rewritten, and its result used where a list is expected.
38# Impossible without type error.
39# Refocus: hazard is when the list itself (the comp) is used where its list-ness matters.
40# And there is a dict([comp]) that gets rewritten - but that's a different expression.
41# So we accept that the hazard may not trigger a visible change unless the rewritten dict comp is
42# used in a list-expecting context - which would break.
43# Example:
44xs = [1,2]
45# This dict([...]) will be rewritten to {x:x for x in xs}
46# But if we then try to use it as a list...
47d = dict([(x, x) for x in xs]) # becomes dict comp
48if hasattr(d, 'append'): # will be False
49 d.append((99,99))
50else:
51 print('dict has no append')
52# Whereas if it stayed a list comp wrapped in dict(), it's still a dict.
53# No difference.
54# Give up and use the eval example from before but add mutation.
55# Final decision: use a function that expects a list and mutates it, and pass the result of
56# a dict([list_comp]) - but that makes no sense.
57# Instead: the list comprehension itself is used elsewhere.
58# Let me just output a coherent example for the next hazard and note this one is best-effort.
mutate list result: 3
final list: [(1, 2), (3, 4), (99, 99)]
list mutability test: 3
h is: <class 'dict'>
b before mutate: [((1, 2), (1, 2))] <class 'list'>
b after mutate: [((1, 2), (1, 2)), (99, 99)]
dict has no append
r2-p006 · Star Imports and Module Side Effects
1import sys
2# Module-level dict comprehension that triggers rule
3MAP = dict([(k, k) for k in ['a', 'b']])
4# Simulate * import side effect by checking what's in globals after
5if 'MAP' in globals():
6 print('MAP defined as:', type(MAP).__name__)
7 print('MAP contents:', MAP)
MAP defined as: dict
MAP contents: {'a': 'a', 'b': 'b'}
r2-p007 · Keyword-Only and Defaults Evaluation
1def make_processor(seq=None):
2 if seq is None:
3 seq = []
4 return lambda: dict([(x, x*2) for x in seq])
5p = make_processor()
6print('first call:', p())
7seq = [1,2,3]
8p2 = make_processor(seq)
9print('second call:', p2())
10# Now mutate seq after defining p2 but before calling
11seq.append(4)
12print('after mutate, p2():', p2())
first call: {}
second call: {1: 2, 2: 4, 3: 6}
after mutate, p2(): {1: 2, 2: 4, 3: 6, 4: 8}
r2-p008 · Pathlib vs os.path Behavior
1import os
2from pathlib import Path
3# Create a temporary file
4open('tmp.txt', 'w').close()
5try:
6 # List comp that dict() wraps
7 pairs = [(str(Path('tmp.txt')), os.path.abspath('tmp.txt')) for _ in range(1)]
8 d = dict(pairs)
9 print('Path str == abspath?', d[str(Path('tmp.txt'))] == d[os.path.abspath('tmp.txt')])
10 # Now the dict comp equivalent (what rule would produce)
11 d2 = {str(Path('tmp.txt')): os.path.abspath('tmp.txt') for _ in range(1)}
12 print('dict comp same:', d2[str(Path('tmp.txt'))] == d2[os.path.abspath('tmp.txt')])
13finally:
14 os.remove('tmp.txt')