ruff RUF024
labelled unknown
Analysis Nemotron 3 Super
Flags calls to dict.fromkeys where the value argument is a mutable literal (list, dict, set).
Fix: Replaces dict.fromkeys(keys, mutable_value) with a dictionary comprehension {key: mutable_value for key in keys} to ensure each value is a distinct instance.
10 hazards
- If 'keys' has side effects (e.g., a function call), evaluating it twice in the comprehension changes behavior.
- If 'mutable_value' has side effects (e.g., a function call like []()), evaluating it per iteration changes behavior vs. single evaluation.
- If 'keys' is not reusable (e.g., an iterator), the second iteration in the comprehension yields no items, resulting in an empty dict.
- If 'keys' is used later in the code after being consumed by the comprehension, it may be exhausted and cause errors.
- If the original code relied on all values being the same object (e.g., for identity checks or shared state), the fix breaks that assumption.
- If 'mutable_value' is a type constructor (e.g., list), the comprehension calls it repeatedly, which is safe but may be less efficient than the original single call.
- If the dict.fromkeys call is part of a larger expression, the rewrite may alter precedence or grouping, leading to syntax errors.
- Comments attached to the dict.fromkeys line may be lost or misplaced during the rewrite.
- If 'keys' contains duplicate elements, the comprehension processes duplicates while dict.fromkeys deduplicates, changing the resulting dictionary's keys.
- If 'mutable_value' is a mutable object referenced elsewhere, the fix creates independent copies, breaking external aliases that expected shared mutation.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Mutable Default Arguments Aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p002 | fired, no fix | Walrus Late Binding Lambda | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p003 | fired, no fix | Keys Exhausted After Rewrite | If 'keys' is used later in the code after being consumed by the comprehension, it may be exhausted and cause errors. |
| r1-p004 | fired, no fix | String Formatting Edge Cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r1-p005 | fired, no fix | Keys Side Effects Evaluated Twice | If 'keys' has side effects (e.g., a function call), evaluating it twice in the comprehension changes behavior. |
| r1-p006 | silent | Context Manager Write Without Close | context managers that are not files, objects with write() but no close() |
| r1-p007 | fired, no fix | Duplicate Keys Deduplication | If 'keys' contains duplicate elements, the comprehension processes duplicates while dict.fromkeys deduplicates, changing the resulting dictionary's keys. |
| r1-p008 | silent prediction off | File Modes Truncation Partial Writes | file modes and truncation, partial writes, non-str data passed to write |
| r2-p001 | silent prediction off | Side effects in keys evaluated twice | If 'keys' has side effects (e.g., a function call), evaluating it twice in the comprehension changes behavior. |
| r2-p002 | silent prediction off | Walrus and scoping in comprehension | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p003 | silent prediction off | Keyword-only and defaults evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p004 | silent prediction off | Duplicate keys in keys | If 'keys' contains duplicate elements, the comprehension processes duplicates while dict.fromkeys deduplicates, changing the resulting dictionary's keys. |
| r2-p005 | silent prediction off | Shared mutable value reliance | If the original code relied on all values being the same object (e.g., for identity checks or shared state), the fix breaks that assumption. |
| r2-p006 | silent prediction off | File modes truncation partial writes non-str data | file modes and truncation, partial writes, non-str data passed to write |
| r2-p007 | silent prediction off | Keys is not reusable iterator | If 'keys' is not reusable (e.g., an iterator), the second iteration in the comprehension yields no items, resulting in an empty dict. |
| r2-p008 | silent prediction off | Context managers not files with write but no close | context managers that are not files, objects with write() but no close() |
Sources
r1-p001 · Mutable Default Arguments Aliasing
1def f(x=dict.fromkeys(['a','b'], [])):
2 x['a'].append(1)
3 return x
4print(f())
5print(f())
{'a': [1], 'b': [1]}
{'a': [1, 1], 'b': [1, 1]}r1-p002 · Walrus Late Binding Lambda
1fs = []
2for i in range(2):
3 if (d := dict.fromkeys(['x','y'], [i])):
4 fs.append(lambda: d['x'])
5print([f() for f in fs])
[[1], [1]]
r1-p003 · Keys Exhausted After Rewrite
1keys = iter(['a','b'])
2d = dict.fromkeys(keys, [])
3print('keys before:', list(keys))
4try:
5 second = dict.fromkeys(keys, [])
6except Exception as e:
7 print(type(e).__name__, str(e))
keys before: []
r1-p004 · String Formatting Edge Cases
1d = dict.fromkeys(['a','b'], ['{}'])
2s = d['a'][0].format('X')
3print(s)
X
r1-p005 · Keys Side Effects Evaluated Twice
1state = []
2def gen():
3 state.append(1)
4 return ['x','y']
5d = dict.fromkeys(gen(), [])
6print('state:', state)
7print('d:', d)
state: [1]
d: {'x': [], 'y': []}r1-p006 · Context Manager Write Without Close
1class Buffer:
2 def __init__(self):
3 self.data = []
4 def write(self, s):
5 self.data.append(s)
6buf = Buffer()
7d = dict.fromkeys(['a','b'], buf)
8d['a'].write('X')
9print(buf.data)
['X']
r1-p007 · Duplicate Keys Deduplication
1keys = ['a','a','b']
2d1 = dict.fromkeys(keys, [])
3d2 = {k: [] for k in keys}
4print('fromkeys keys:', list(d1.keys()))
5print('comp keys:', list(d2.keys()))
fromkeys keys: ['a', 'b'] comp keys: ['a', 'b']
r1-p008 · File Modes Truncation Partial Writes
1with open('tmp.txt', 'w') as f:
2 pass
3with open('tmp.txt', 'r+') as f:
4 d = dict.fromkeys(['a','b'], f)
5 d['a'].write('AB')
6 f.seek(0)
7 print(repr(f.read()))
8import os; os.remove('tmp.txt')
'AB'
r2-p001 · Side effects in keys evaluated twice
1def get_keys():
2 print('get_keys called')
3 return ['a', 'b']
4
5# Before fix: dict.fromkeys(get_keys(), [])
6# After fix: {k: [] for k in get_keys()}
7d = {k: [] for k in get_keys()}
8print('Number of calls:', d.get('a', None) is not None and 1 or 0)
get_keys called Number of calls: 1
r2-p002 · Walrus and scoping in comprehension
1keys = ['a', 'b']
2# Before fix: dict.fromkeys(keys, [])
3# After fix: {k: [] for k in keys}
4fns = []
5for k in keys:
6 fns.append(lambda: k)
7# Capture current k via walrus in comprehension (simulating post-fix behavior)
8d = {(k := k): [] for k in keys} # This line is the fix target
9print('Lambda results:', [f() for f in fns])
10print('Dict keys:', list(d.keys()))
r2-p003 · Keyword-only and defaults evaluated once
1def make_dict(*, keys, value=[]):
2 return {k: value for k in keys}
3
4# Before fix: dict.fromkeys(keys, [])
5# After fix: {k: [] for k in keys} --> but here we use default arg
6keys = ['a', 'b']
7d1 = make_dict(keys=keys)
8d2 = make_dict(keys=keys)
9d1['a'].append(1)
10print('d2["a"]:', d2['a']) # Shared due to default
d2["a"]: [1]
r2-p004 · Duplicate keys in keys
1keys = ['a', 'b', 'a'] # duplicate
2# Before fix: dict.fromkeys(keys, []) -> {'a': [], 'b': []}
3# After fix: {k: [] for k in keys} -> {'a': [], 'b': [], 'a': []} but dict dedupes literals? No: comprehension builds dict, last wins
4# Actually: dict comprehension also deduplicates by key, last value wins.
5# So same result? Wait: hazard says comprehension processes duplicates -> but dict keys are unique.
6# Correction: both dict.fromkeys and dict comprehension have same key deduplication.
7# So this hazard is invalid? But per prompt, we must use it.
8# Let's instead show that if keys had side effects per element...
9# But per instructions, we must use the hazard as stated.
10# Assume the user means: comprehension iterates over all elements, dict.fromkeys also iterates over all but dedups in dict.
11# Same iteration count. So no diff in count.
12# But let's output something to show we processed duplicates.
13count = 0
14def count_keys(iterable):
15 nonlocal count
16 for k in iterable:
17 count += 1
18 yield k
19
20keys_iter = count_keys(['a', 'b', 'a'])
21d = {k: [] for k in keys_iter}
22print('Iteration count:', count)
23print('Dict:', d)
r2-p005 · Shared mutable value reliance
1keys = ['a', 'b']
2# Before fix: d = dict.fromkeys(keys, []) # all values same list
3# After fix: d = {k: [] for k in keys} # each value different list
4d = {k: [] for k in keys}
5print('Same object?', d['a'] is d['b'])
6d['a'].append(1)
7print('d["b"]:', d['b']) # Should be [] if fix applied
Same object? False d["b"]: []
r2-p006 · File modes truncation partial writes non-str data
1import io
2# Simulate writing non-str to buffer via .write()
3buf = io.BytesIO()
4# Before fix: dict.fromkeys(['a'], buf.write) # but write expects bytes
5# After fix: {k: buf.write for k in ['a']}
6# We can't call dict.fromkeys with non-callable second arg meaningfully.
7# Instead: store the write method, then call it later.
8# Hazard: if we had used dict.fromkeys, all keys share same method binding? No, methods are same function.
9# But comprehension creates new references? Still same function.
10# Let's instead show that if we had stored mutable state in the value...
11# But per hint, we must involve file modes/truncation.
12# Use a file.
13with open('test.txt', 'w+') as f:
14 pass # truncate
15# Now simulate: suppose we wanted to store a file handle's write method per key, but all should write to same file.
16# Not meaningful.
17# Alternate interpretation: the value is a list used to collect written data, but shared.
18# Given constraints, we do:
19d = {k: [] for k in ['x', 'y']} # fix form
20try:
21 d['x'].append(b'hello')
22 # Simulate partial write: if we had used shared list, both would see it.
23 print('d["y"]:', d['y'])
24except Exception as e:
25 print(type(e).__name__, str(e))
26finally:
27 import os
28 if os.path.exists('test.txt'):
29 os.remove('test.txt')
d["y"]: []
r2-p007 · Keys is not reusable iterator
1def make_iter():
2 return iter(['a', 'b'])
3keys_iter = make_iter()
4# Before fix: dict.fromkeys(keys_iter, []) # consumes iterator once
5# After fix: {k: [] for k in keys_iter} # same consumption
6# But hazard says second iteration yields nothing -> only if we iterate twice.
7# So we simulate: code that uses keys twice.
8# Example: if someone did dict.fromkeys(keys, []) and then later used keys again.
9# But the fix itself doesn't cause double iteration.
10# Per hazard: "the second iteration in the comprehension" -> the fix does not have a second iteration.
11# Wait: the fix is a single comprehension.
12# Unless the hazard means: if the original code was inside a loop that runs twice...
13# Given confusion, we follow: if keys is an iterator, and we (somehow) evaluate it twice.
14# Let's force two iterations by using it in two places.
15# But the fix only uses it once.
16# Instead, we show that if the code were rewritten incorrectly to iterate twice, it would break.
17# But we must show the fix causing issue.
18# Alternative: the user may think the fix evaluates keys twice? It does not.
19# We comply by showing that if keys is an iterator, using it in a comprehension that is itself inside a loop that re-evaluates the comprehension...
20# Too complex.
21# Simple: show that dict.fromkeys works with iterator (consumes once), and comprehension also works (consumes once). Same.
22# To show difference, we need the original code to have been safe because it didn't consume keys? No.
23# Let's instead use an iterator that raises after first yield.
24class BrokenIter:
25 def __init__(self):
26 self.count = 0
27 def __iter__(self):
28 return self
29 def __next__(self):
30 if self.count >= 1:
31 raise StopIteration
32 self.count += 1
33 return 'a'
34
35keys_iter = BrokenIter()
36# Before fix: dict.fromkeys(keys_iter, []) -> {'a': []}
37# After fix: {k: [] for k in keys_iter} -> {'a': []}
38# Same.
39# So we cannot show difference.
40# We output what we can.
41d = {k: [] for k in keys_iter}
42print('Result:', d)
Result: {'a': []}r2-p008 · Context managers not files with write but no close
1class Writer:
2 def __init__(self):
3 self.data = []
4 def write(self, s):
5 self.data.append(s)
6 def __enter__(self):
7 return self
8 def __exit__(self, *exc):
9 pass # no close
10
11# Before fix: dict.fromkeys(['a'], Writer()) # all keys share same Writer instance
12# After fix: {k: Writer() for k in ['a']} # each key gets new Writer
13d = {k: Writer() for k in ['x', 'y']}
14d['x'].write('hello')
15print('d["y"].data:', d['y'].data) # Should be [] if fix applied
d["y"].data: []