ruff SIM911
labelled unknown
Analysis Nemotron 3 Super
Flags usage of `zip(dict.keys(), dict.values())` when iterating over both simultaneously.
Fix: Replace `zip(expr.keys(), expr.values())` with `expr.items()`, where `expr` is the same dictionary expression in both calls.
10 hazards
- If `expr` has side effects (e.g., a function call), evaluating it twice in the original vs once in the fix changes behavior.
- If `expr` is mutated between the `.keys()` and `.values()` calls, the zip may see inconsistent state while `.items()` sees a snapshot.
- If `expr` is not a dict but a subclass with overridden `.keys()` or `.values()` that return non-corresponding iterables, the zip may pair incorrectly while `.items()` is correct.
- If `.keys()` or `.values()` returns a custom iterator with state that affects iteration (e.g., depends on prior calls), replacing with `.items()` alters execution.
- If the expression uses a walrus operator (e.g., `zip((d:=func()).keys(), (d:=func()).values())`), the fix would lose reassignment and break scoping.
- If the code relies on the order of evaluation (e.g., `.keys()` has a side effect that must happen before `.values()`), the fix may reverse or eliminate ordering expectations.
- If `expr` is a variable that gets rebound between the two attribute accesses (e.g., due to concurrency or descriptor magic), the fix uses a single consistent value while the original may see two different ones.
- If the original code contains inline comments within the zip expression (e.g., `zip(d.keys() # comment, d.values())`), the fix may drop or misplace them during rewrite.
- If `expr` is a property that returns a different dict on each access, the original zips keys from one dict with values from another, while the fix uses items from a single dict (possibly inconsistent).
- If `expr` is `None` or an object that raises AttributeError on `.keys()` or `.values()` only under certain conditions (e.g., due to `__getattr__`), the fix may trigger the error earlier/later or not at all.
Findings
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r1-p006 | fix-changes-behavior | labelled safe | real · high | related | The rule rewrites `zip(d.keys(), d.values())` to `d.items()`, but `zip` returns a reusable iterator while `dict.items()` returns a view that reflects the dicti… |
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Truthiness of Custom Bool/Len | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r1-p002 | invalid | Dict Ordering & Duplicates | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
| r1-p003 | fired, no fix | Inline Comments in Zip Expression | If the original code contains inline comments within the zip expression, the fix may drop or misplace them during rewrite. |
| r1-p004 | silent prediction off | Order of Evaluation Side Effects | If the code relies on the order of evaluation (e.g., `.keys()` has a side effect that must happen before `.values()`), the fix may reverse or eliminate ordering expectations. |
| r1-p005 | silent prediction off | Conditional AttributeError via __getattr__ | If `expr` is `None` or an object that raises AttributeError on `.keys()` or `.values()` only under certain conditions (e.g., due to `__getattr__`), the fix may trigger the error earlier/later or not a |
| r1-p006 | fix-changes-behavior | String Formatting Edge Cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r1-p007 | silent prediction off | Custom Iterator State in Keys/Values | If `.keys()` or `.values()` returns a custom iterator with state that affects iteration (e.g., depends on prior calls), replacing with `.items()` alters execution. |
| r1-p008 | fix ok | Bytes vs Str Encoding | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p001 | fix ok | String Formatting Edge Cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p002 | fix ok | Bytes vs Str Encoding Newline Handling | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p003 | silent prediction off | Side Effect in Expr Evaluation | If `expr` has side effects (e.g., a function call), evaluating it twice in the original vs once in the fix changes behavior. |
| r2-p004 | silent prediction off | Mutable Default Arguments and Aliasing | mutable default arguments and aliasing after a rewrite |
| r2-p005 | fix ok | Type Annotations Evaluated at Runtime | type annotations evaluated at runtime, typing-only imports |
| r2-p006 | fired, no fix | Inline Comments in Zip Expression | If the original code contains inline comments within the zip expression (e.g., `zip(d.keys() # comment, d.values())`), the fix may drop or misplace them during rewrite. |
| r2-p007 | silent prediction off | Variable Rebound Between Attribute Accesses | If `expr` is a variable that gets rebound between the two attribute accesses (e.g., due to concurrency or descriptor magic), the fix uses a single consistent value while the original may see two diffe |
| r2-p008 | fix ok | Async Def/Await Ordering Async Generators | async def/await ordering, async generators |
Sources
r1-p001 · Truthiness of Custom Bool/Len
1class D:
2 def __init__(self, n):
3 self.n = n
4 def keys(self):
5 return range(self.n)
6 def values(self):
7 return range(self.n)
8 def items(self):
9 return [(i, i) for i in range(self.n)]
10 def __bool__(self):
11 return self.n > 0
12 def __len__(self):
13 return self.n
14
15d = D(2)
16print('Before:', bool(d))
17trace = []
18for k, v in zip(d.keys(), d.values()):
19 trace.append((k, v))
20print('Zip result:', trace)
21d.n = 0
22print('After zero:', bool(d))
23try:
24 for k, v in zip(d.keys(), d.values()):
25 pass
26except Exception as e:
27 print('Error:', type(e).__name__, str(e))
Before: True Zip result: [(0, 0), (1, 1)] After zero: False
r1-p002 · Dict Ordering & Duplicates (generated program is not deterministic)
1class K:
2 def __init__(self, v):
3 self.v = v
4 def __hash__(self):
5 return hash(self.v)
6 def __eq__(self, other):
7 return isinstance(other, K) and self.v == other.v
8
9d = {K(1): 'a', K(1): 'b', K(2): 'c'} # duplicate key
10print('Dict:', d)
11trace_keys = list(d.keys())
12trace_vals = list(d.values())
13trace_items = list(d.items())
14print('Keys:', [k.v for k in trace_keys])
15print('Values:', trace_vals)
16print('Items:', [(k.v, v) for k, v in trace_items])
17zipped = list(zip(d.keys(), d.values()))
18print('Zipped:', [(k.v, v) for k, v in zipped])
19if zipped != list(d.items()):
20 print('MISMATCH')
r1-p003 · Inline Comments in Zip Expression
1d = {'a': 1, 'b': 2}
2# This comment should stay
3result = list(zip(d.keys() # keys comment
4 , d.values())) # values comment
5print('Result:', result)
6# After fix, comments may be lost
7print('Expected:', list(d.items()))
Result: [('a', 1), ('b', 2)]
Expected: [('a', 1), ('b', 2)]r1-p004 · Order of Evaluation Side Effects
1state = []
2class DictLike:
3 def keys(self):
4 state.append('keys')
5 return {'x': 1}.keys()
6 def values(self):
7 state.append('values')
8 return {'x': 1}.values()
9 def items(self):
10 state.append('items')
11 return {'x': 1}.items()
12
13d = DictLike()
14print('State before:', state)
15_ = list(zip(d.keys(), d.values())) # keys() then values()
16print('State after zip:', state)
17state.clear()
18_ = list(d.items()) # items() call
19print('State after items:', state)
State before: [] State after zip: ['keys', 'values'] State after items: ['items']
r1-p005 · Conditional AttributeError via __getattr__
1class MaybeDict:
2 def __init__(self, fail_on_keys=False):
3 self.fail_on_keys = fail_on_keys
4 self._data = {'a': 1}
5 def __getattr__(self, name):
6 if name == 'keys' and self.fail_on_keys:
7 raise AttributeError('keys failed')
8 if name == 'values':
9 return self._data.values()
10 if name == 'items':
11 return self._data.items()
12 raise AttributeError(name)
13
14obj = MaybeDict(fail_on_keys=True)
15print('Testing zip...')
16try:
17 list(zip(obj.keys(), obj.values()))
18except AttributeError as e:
19 print('Zip error:', str(e))
20obj2 = MaybeDict(fail_on_keys=False)
21print('Testing items...')
22try:
23 list(obj2.items())
24except AttributeError as e:
25 print('Items error:', str(e))
Testing zip... Zip error: keys failed Testing items...
r1-p006 · String Formatting Edge Cases
1d = {'a': 1, 'b': 2}
2zipped = zip(d.keys(), d.values())
3for k, v in zipped:
4 s = f'{k!r}:{v!r}'
5 print(s)
6# After fix, same output expected
7items = d.items()
8for k, v in items:
9 s = '{!r}:{!r}'.format(k, v)
10 print(s)
11# Test repr vs str
12print(repr(list(zipped)))
13print(str(list(items)))
--- orig.py 2026-10-17 16:38:58.621792777 -0400 +++ fixed.py 2026-10-17 16:38:58.785800554 -0400 @@ -1,5 +1,5 @@ d = {'a': 1, 'b': 2} -zipped = zip(d.keys(), d.values()) +zipped = d.items() for k, v in zipped: s = f'{k!r}:{v!r}' print(s)
'a':1
'b':2
'a':1
'b':2
[]
[('a', 1), ('b', 2)]r1-p007 · Custom Iterator State in Keys/Values
1class CounterIter:
2 def __init__(self, data):
3 self.data = data
4 self.count = 0
5 def __iter__(self):
6 return self
7 def __next__(self):
8 if self.count >= len(self.data):
9 raise StopIteration
10 result = self.data[self.count]
11 self.count += 1
12 return result
13
14class DictLike:
15 def __init__(self):
16 self._keys = ['a', 'b']
17 self._vals = [1, 2]
18 def keys(self):
19 return CounterIter(self._keys)
20 def values(self):
21 return CounterIter(self._vals)
22 def items(self):
23 return zip(self._keys, self._vals)
24
25d = DictLike()
26print('First zip:', list(zip(d.keys(), d.values())))
27print('Second zip:', list(zip(d.keys(), d.values()))) # exhausted
28print('Items:', list(d.items()))
First zip: [('a', 1), ('b', 2)]
Second zip: [('a', 1), ('b', 2)]
Items: [('a', 1), ('b', 2)]r1-p008 · Bytes vs Str Encoding
1d = {b'a\nb': b'1\n2', b'c': b'3'}
2zipped = list(zip(d.keys(), d.values()))
3for k, v in zipped:
4 print(repr(k), repr(v))
5# Simulate text processing
6try:
7 for k, v in d.items():
8 print(k.decode(), v.decode())
9except Exception as e:
10 print('Decode error:', type(e).__name__, str(e))
11# Test newline handling
12with open('test.txt', 'wb') as f:
13 for k, v in d.items():
14 f.write(k + b':' + v + b'\n')
15with open('test.txt', 'rb') as f:
16 print(f.read())
17import os
18os.remove('test.txt')
--- orig.py 2026-10-17 16:38:59.017811553 -0400 +++ fixed.py 2026-10-17 16:38:59.180819270 -0400 @@ -1,5 +1,5 @@ d = {b'a\nb': b'1\n2', b'c': b'3'} -zipped = list(zip(d.keys(), d.values())) +zipped = list(d.items()) for k, v in zipped: print(repr(k), repr(v)) # Simulate text processing
b'a\nb' b'1\n2' b'c' b'3' a b 1 2 c 3 b'a\nb:1\n2\nc:3\n'
r2-p001 · String Formatting Edge Cases
1d = {'k': '%\n', 'v': '\t'}
2k, v = zip(d.keys(), d.values())
3s = ''.join(f'{a!r}:{b!r}' for a, b in k)
4print(s)
--- orig.py 2026-10-17 16:39:03.757035241 -0400 +++ fixed.py 2026-10-17 16:39:04.156054002 -0400 @@ -1,4 +1,4 @@ d = {'k': '%\n', 'v': '\t'} -k, v = zip(d.keys(), d.values()) +k, v = d.items() s = ''.join(f'{a!r}:{b!r}' for a, b in k) print(s)
r2-p002 · Bytes vs Str Encoding Newline Handling
1d = {'a': b'hello\nworld', 'b': b'foo\r\nbar'}
2pairs = list(zip(d.keys(), d.values()))
3for k, v in pairs:
4 print(k.decode(), v.decode().replace('\n', '\\n'))
--- orig.py 2026-10-17 16:39:04.367063913 -0400 +++ fixed.py 2026-10-17 16:39:04.751081951 -0400 @@ -1,4 +1,4 @@ d = {'a': b'hello\nworld', 'b': b'foo\r\nbar'} -pairs = list(zip(d.keys(), d.values())) +pairs = list(d.items()) for k, v in pairs: print(k.decode(), v.decode().replace('\n', '\\n'))
r2-p003 · Side Effect in Expr Evaluation
1calls = []
2def get_dict():
3 calls.append(len(calls))
4 return {'x': 1, 'y': 2}
5k, v = zip(get_dict().keys(), get_dict().values())
6print(list(k), list(v), calls)
['x', 1] ['y', 2] [0, 1]
r2-p004 · Mutable Default Arguments and Aliasing
1def f(d={'a': 1, 'b': 2}):
2 pairs = list(zip(d.keys(), d.values()))
3 d['c'] = 3
4 return pairs
5print(f())
6print(f({'x': 9}))
[('a', 1), ('b', 2)]
[('x', 9)]r2-p005 · Type Annotations Evaluated at Runtime
1from typing import Dict, Any
2D: Dict[str, int] = {'a': 1}
3ann = D.__annotations__ if hasattr(D, '__annotations__') else {}
4k, v = zip(D.keys(), D.values())
5print(list(k), list(v), ann)
--- orig.py 2026-10-17 16:39:05.223104106 -0400 +++ fixed.py 2026-10-17 16:39:05.607122116 -0400 @@ -1,5 +1,5 @@ from typing import Dict, Any D: Dict[str, int] = {'a': 1} ann = D.__annotations__ if hasattr(D, '__annotations__') else {} -k, v = zip(D.keys(), D.values()) +k, v = D.items() print(list(k), list(v), ann)
r2-p006 · Inline Comments in Zip Expression
1d = {'x': 1, 'y': 2}
2k, v = zip(d.keys() # comment
3 , d.values())
4print(list(k), list(v))
['x', 1] ['y', 2]
r2-p007 · Variable Rebound Between Attribute Accesses
1class DictHolder:
2 def __init__(self):
3 self.d = {'a': 1}
4 @property
5 def keys(self):
6 return self.d.keys()
7 @property
8 def values(self):
9 self.d = {'b': 2}
10 return self.d.values()
11h = DictHolder()
12k, v = zip(h.keys, h.values)
13print(list(k), list(v))
r2-p008 · Async Def/Await Ordering Async Generators
1async def gen():
2 d = {'a': 1, 'b': 2}
3 for k, v in zip(d.keys(), d.values()):
4 yield k, v
5import asyncio
6async def main():
7 result = [pair async for pair in gen()]
8 print(result)
9asyncio.run(main())
--- orig.py 2026-10-17 16:39:06.347156796 -0400 +++ fixed.py 2026-10-17 16:39:06.619169533 -0400 @@ -1,6 +1,6 @@ async def gen(): d = {'a': 1, 'b': 2} - for k, v in zip(d.keys(), d.values()): + for k, v in d.items(): yield k, v import asyncio async def main():
[('a', 1), ('b', 2)]