ruff C406
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Dict subclass check | Rewriting dict(L) to {k:v for k,v in L} produces a plain dict, breaking isinstance checks against a custom dict subclass. |
| r1-p002 | fired, no fix | Exception handling in dict construction | Exceptions 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-p003 | silent prediction off | Side effect timing in iterable | Moving side effects (prints) from list/tuple into dict literal comprehension preserves order but changes when the iterable is materialized relative to surrounding expressions. |
| r1-p004 | silent prediction off | Aliasing and mutation | If the list/tuple is aliased and mutated after dict() call, the rewrite breaks because it no longer references the original object. |
| r1-p005 | fired, no fix | Unhashable keys failure order | Both forms fail with unhashable keys, but dict literal comprehension may fail during hashing in a different order than dict() constructor. |
| r1-p006 | silent | Pathlib vs os.path (irrelevant to rule) | This hazard is unrelated to dict() rewriting; included to show silence on near-miss. |
| r1-p007 | silent | Local dict shadowing bypass | If '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-p008 | fired, no fix | Lost comments in AST rewrite | Comments attached to list/tuple brackets or commas are lost when replacing dict(L) with {k:v for k,v in L}. |
| r2-p001 | silent prediction off | Rebound dict builtin | 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 |
| r2-p002 | silent | Descriptor with side effect in __get__ | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p003 | silent prediction off | Aliasing and mutation after dict() call | 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 i |
| r2-p004 | silent | File write with truncated data | file modes and truncation, partial writes, non-str data passed to write |
| r2-p005 | silent | Star import and module level side effect | star-imports, module-level side effects, __all__ |
| r2-p006 | silent | Pathlib vs os.path symlink resolution | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r2-p007 | silent prediction off | Exception in dict construction with finally | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p008 | silent prediction off | Generator consumed once vs reusable list | generators 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: {}