fixhunt

ruff C406

labelled unknown
Runs / 2026-10-17T20-20-15-856Z-kjno-ruff-C406ruff 0.17.12026-10-17 20:20 UTC8 programs × 2 round(s)16 programs, 4 fired, 0 fixed, 0 findings, 0 rejectedmodel cost $0.0045

Analysis Nemotron 3 Super

Flags calls to dict() constructed with list/tuple literals containing key-value pairs.

Fix: Rewrites dict(L) where L is a list or tuple of 2-element iterables into a dict literal {k: v for k, v in L}, preserving order and duplicates as per Python semantics.

11 hazards
  • If the list/tuple contains duplicate keys, the dict literal preserves last-wins behavior identical to dict() constructor, but if the iterable is not actually a sequence of pairs (e.g., flat list), the comprehension may raise ValueError during construction where dict() would have succeeded by treating elements as pairs via iteration.
  • If the input iterable is not actually iterable (e.g., None), both forms raise TypeError, but the rewrite may fail earlier during comprehension setup if the type is incompatible with iteration.
  • If the list/tuple expression contains side effects (e.g., function calls with prints), moving them into a dict literal comprehension preserves evaluation order but may alter timing relative to surrounding expressions due to changes in when the iterable is materialized.
  • If the list/tuple is used elsewhere after the dict() call (aliasing), the rewrite creates a new dict and does not preserve the original list/tuple, potentially breaking code that mutates or inspects it later.
  • If the dict() call appears in a context where its return value is ignored but the list/tuple construction has side effects (e.g., dict([print('x'), (1,2)][0])), the rewrite may drop those side effects if the comprehension short-circuits or fails to evaluate all elements.
  • If the list/tuple contains unhashable keys (e.g., lists), both forms fail at runtime with TypeError, but the dict literal comprehension may fail during key hashing in a different order than the constructor's internal processing.
  • If the code relies on the exact type being dict (not a subclass), the rewrite produces a built-in dict, which is equivalent for all standard uses but may break isinstance checks against a custom dict subclass if one was monkey-patched or expected via factory.
  • If the list/tuple literal contains nested mutable objects that are mutated during iteration (e.g., dict([(x.append(1), x.pop()) for x in [[], []]])), the rewrite preserves mutation order but may interact poorly with optimizations or early termination in comprehensions.
  • Comments attached to the list/tuple brackets or commas may be lost during AST rewrite, as the autofix replaces the entire node with a dict literal and does not transfer associated comment nodes.
  • If the dict() call is part of a larger expression where operator precedence groups unexpectedly (e.g., dict([(1,2)]) + 3 vs {(1,2):} + 3), the rewrite does not change precedence because the dict literal binds tighter than addition, but the original call already had dict() as a unit—so no change in meaning, only risk is if parentheses were omitted and relied on for grouping.
  • In rare cases involving __dict__ or descriptor interactions, replacing a call with a literal may bypass __call__ overrides if dict is shadowed locally, but the rule only triggers on built-in dict, so this is unsafe only if 'dict' is rebound in scope.

Programs

idresulttitlehazard
r1-p001fired, no fixDict subclass checkRewriting dict(L) to {k:v for k,v in L} produces a plain dict, breaking isinstance checks against a custom dict subclass.
r1-p002fired, no fixException handling in dict constructionExceptions raised during dict() construction (e.g., in key/value expressions) are preserved in order, but finally/else clauses and exception chaining may differ due to AST rewrite timing.
r1-p003silent prediction offSide effect timing in iterableMoving side effects (prints) from list/tuple into dict literal comprehension preserves order but changes when the iterable is materialized relative to surrounding expressions.
r1-p004silent prediction offAliasing and mutationIf the list/tuple is aliased and mutated after dict() call, the rewrite breaks because it no longer references the original object.
r1-p005fired, no fixUnhashable keys failure orderBoth forms fail with unhashable keys, but dict literal comprehension may fail during hashing in a different order than dict() constructor.
r1-p006silentPathlib vs os.path (irrelevant to rule)This hazard is unrelated to dict() rewriting; included to show silence on near-miss.
r1-p007silentLocal dict shadowing bypassIf 'dict' is rebound in scope, the rule does not fire (it only triggers on built-in dict), but if it did, the rewrite would bypass __call__ overrides.
r1-p008fired, no fixLost comments in AST rewriteComments attached to list/tuple brackets or commas are lost when replacing dict(L) with {k:v for k,v in L}.
r2-p001silent prediction offRebound dict builtinIn rare cases involving __dict__ or descriptor interactions, replacing a call with a literal may bypass __call__ overrides if dict is shadowed locally, but the rule only triggers on built-in dict, so
r2-p002silentDescriptor with side effect in __get__class attributes vs instance attributes, descriptors, properties with side effects
r2-p003silent prediction offAliasing and mutation after dict() callIf the list/tuple is used elsewhere after the dict() call (aliasing), the rewrite creates a new dict and does not preserve the original list/tuple, potentially breaking code that mutates or inspects i
r2-p004silentFile write with truncated datafile modes and truncation, partial writes, non-str data passed to write
r2-p005silentStar import and module level side effectstar-imports, module-level side effects, __all__
r2-p006silentPathlib vs os.path symlink resolutionpathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths
r2-p007silent prediction offException in dict construction with finallyexceptions raised inside the rewritten code, finally/else clauses, exception chaining
r2-p008silent prediction offGenerator consumed once vs reusable listgenerators vs lists, iterators consumed once, lazy vs eager evaluation

Sources

r1-p001 · Dict subclass check
1class MyDict(dict): pass
2original = dict([(1, 2)])
3rewritten = {(k, v) for k, v in [(1, 2)]}
4print(type(original), type(rewritten))
5print(isinstance(original, MyDict), isinstance(rewritten, MyDict))
<class 'dict'> <class 'set'>
False False
r1-p002 · Exception handling in dict construction
1def f():
2 try:
3 d = dict([(1, 2), (3, 1/0)])
4 except ZeroDivisionError as e:
5 print('caught:', type(e).__name__)
6 else:
7 print('no error')
8 finally:
9 print('finally')
10f()
11try:
12 d = {(k, v) for k, v in [(1, 2), (3, 1/0)]}
13except ZeroDivisionError as e:
14 print('caught:', type(e).__name__)
15else:
16 print('no error')
17finally:
18 print('finally')
caught: ZeroDivisionError
finally
caught: ZeroDivisionError
finally
r1-p003 · Side effect timing in iterable
1def side():
2 print('side')
3 return [('a', 1)]
4print('before')
5d = dict(side())
6print('after')
7print('before')
8d = {k: v for k, v in side()}
9print('after')
before
side
after
before
side
after
r1-p004 · Aliasing and mutation
1pairs = [('a', 1)]
2d = dict(pairs)
3pairs.append(('b', 2))
4print('pairs:', pairs)
5print('d:', d)
6pairs = [('a', 1)]
7d = {k: v for k, v in pairs}
8pairs.append(('b', 2))
9print('pairs:', pairs)
10print('d:', d)
pairs: [('a', 1), ('b', 2)]
d: {'a': 1}
pairs: [('a', 1), ('b', 2)]
d: {'a': 1}
r1-p005 · Unhashable keys failure order
1try:
2 dict([([1, 2], 3)])
3except TypeError as e:
4 print('dict():', e)
5try:
6 {k: v for k, v in [([1, 2], 3)]}
7except TypeError as e:
8 print('{}:', e)
dict(): cannot use 'list' as a dict key (unhashable type: 'list')
{}: cannot use 'list' as a dict key (unhashable type: 'list')
r1-p006 · Pathlib vs os.path (irrelevant to rule)
1import os
2import pathlib
3p = pathlib.Path('.')
4print('pathlib exists:', p.exists())
5print('os.path exists:', os.path.exists('.'))
pathlib exists: True
os.path exists: True
r1-p007 · Local dict shadowing bypass
1def dict(*args): return {'mocked': True}
2d = dict([(1, 2)]) # rule does NOT fire here because dict is not built-in
3print(d)
{'mocked': True}
r1-p008 · Lost comments in AST rewrite
1d = dict([ # x
2 (1, 2) # y
3 ]) # z
4print(d)
5d = { # x
6 k: v # y
7 for k, v in [(1, 2)] # z
8}
9print(d)
{1: 2}
{1: 2}
r2-p001 · Rebound dict builtin
1def f():
2 dict = lambda x: ['shadow']
3 result = dict([(1, 2)])
4 print(result)
5f()
['shadow']
r2-p002 · Descriptor with side effect in __get__
1class Desc:
2 def __get__(self, obj, objtype):
3 print('desc get')
4 return [(1, 2)]
5
6class A:
7 x = Desc()
8
9a = A()
10try:
11 # Access via class triggers descriptor
12 result = dict(A.x)
13except Exception as e:
14 print(type(e).__name__, str(e))
15print('done')
desc get
done
r2-p003 · Aliasing and mutation after dict() call
1pairs = [(1, 2)]
2d = dict(pairs)
3pairs.append((3, 4))
4print('pairs:', pairs)
5print('d:', d)
pairs: [(1, 2), (3, 4)]
d: {1: 2}
r2-p004 · File write with truncated data
1with open('test.txt', 'w') as f:
2 data = [(b'hello', 5)]
3 d = dict(data)
4 try:
5 f.write(str(d)) # This will work
6 except Exception as e:
7 print(type(e).__name__, str(e))
8with open('test.txt', 'r') as f:
9 print(f.read())
{b'hello': 5}
r2-p005 · Star import and module level side effect
1import sys
2mod = type('m', (), {})()
3sys.modules['m'] = mod
4mod._side = print('module side effect')
5# This won't trigger rule, testing isolation
6if hasattr(mod, '_side'):
7 print('side effect seen')
module side effect
side effect seen
r2-p006 · Pathlib vs os.path symlink resolution
1import os
2import tempfile
3with tempfile.TemporaryDirectory() as tmp:
4 link = os.path.join(tmp, 'link')
5 target = os.path.join(tmp, 'target')
6 with open(target, 'w') as f:
7 f.write('yes')
8 os.symlink(target, link)
9 # Not a dict() call shape, just testing path behavior
10 print(os.path.exists(link))
True
r2-p007 · Exception in dict construction with finally
1pairs = [(1, 2), (3, 4)]
2try:
3 d = dict(pairs)
4 raise ValueError('bad')
5except ValueError as e:
6 print('caught:', e)
7finally:
8 print('finally block')
9print('d exists:', 'd' in locals())
caught: bad
finally block
d exists: True
r2-p008 · Generator consumed once vs reusable list
1def gen():
2 yield (1, 2)
3 yield (3, 4)
4g = gen()
5d1 = dict(g) # First consumption
6print('d1:', d1)
7try:
8 d2 = dict(g) # Second consumption - exhausted
9 print('d2:', d2)
10except Exception as e:
11 print('error on second:', type(e).__name__, str(e))
d1: {1: 2, 3: 4}
d2: {}