ruff SIM118
labelled unknown
Analysis Nemotron 3 Super
Flags redundant .keys() usage in membership tests like 'x in d.keys()' where direct 'x in d' is preferred.
Fix: Rewrites 'key in obj.keys()' to 'key in obj' by removing the redundant .keys() attribute access while preserving the membership test semantics.
8 hazards
- If obj is not a dict but has a .keys() method returning a non-container (e.g., generator), removing .keys() may cause TypeError during membership test.
- If obj.keys() has side effects (e.g., prints, state changes), removing the call eliminates those effects.
- If obj is a property that returns a dict-like object with .keys(), accessing obj directly may trigger different behavior than obj.keys() due to property logic.
- If obj is None or lacks __contains__, the rewrite may raise AttributeError instead of returning False.
- If obj is a custom class where __contains__ differs from membership in .keys() (violating dict contract), semantics change.
- If the expression uses short-circuiting and obj.keys() is evaluated conditionally, removing .keys() may evaluate obj earlier/later.
- If obj.keys() is used elsewhere in the same line for another purpose (e.g., assignment), the rewrite breaks that usage.
- If obj is a built-in type like list (which has no .keys()), the rule shouldn't fire, but if misapplied, the rewrite would fail.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Custom class with overridden __contains__ | If obj is a custom class where __contains__ differs from membership in .keys() (violating dict contract), semantics change. |
| r1-p002 | fix ok | Context manager without close() but with write() | context managers that are not files, objects with write() but no close() |
| r1-p003 | fired, no fix | Property with side effects on .keys() access | class attributes vs instance attributes, descriptors, properties with side effects |
| r1-p004 | fix ok prediction off | Runtime type annotation with typing import | type annotations evaluated at runtime, typing-only imports |
| r1-p005 | fired, no fix | Property returning dict-like with different behavior | If obj is a property that returns a dict-like object with .keys(), accessing obj directly may trigger different behavior than obj.keys() due to property logic. |
| r1-p006 | invalid | Keyword-only and positional-only args with default evaluation | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p007 | fired, no fix prediction off | Misapplication to list (no .keys()) - should not fire | If obj is a built-in type like list (which has no .keys()), the rule shouldn't fire, but if misapplied, the rewrite would fail. |
| r1-p008 | fix ok | String formatting with nested quotes and f-strings | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p001 | fired, no fix | Side effect in keys() method | If obj.keys() has side effects (e.g., prints, state changes), removing the call eliminates those effects. |
| r2-p002 | fix ok | String formatting with nested quotes and f-strings | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p003 | fired, no fix prediction off | Built-in type without keys() | If obj is a built-in type like list (which has no .keys()), the rule shouldn't fire, but if misapplied, the rewrite would fail. |
| r2-p004 | fired, no fix | Star-imports and module-level side effects | star-imports, module-level side effects, __all__ |
| r2-p005 | silent prediction off | Class attributes vs instance attributes, descriptors, properties with side effects | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p006 | invalid | Keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p007 | fix ok | Short-circuiting and conditional evaluation of obj.keys() | If the expression uses short-circuiting and obj.keys() is evaluated conditionally, removing .keys() may evaluate obj earlier/later. |
| r2-p008 | fix ok | Mutable default arguments and aliasing after a rewrite | mutable default arguments and aliasing after a rewrite |
Sources
r1-p001 · Custom class with overridden __contains__
1class BadDict:
2 def keys(self):
3 return ['a', 'b']
4 def __contains__(self, item):
5 return item == 'special'
6d = BadDict()
7print('a in d:', 'a' in d)
8print('a in d.keys():', 'a' in d.keys())
a in d: False a in d.keys(): True
r1-p002 · Context manager without close() but with write()
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, *args):
9 pass
10with Writer() as w:
11 d = {'x': 1}
12 if 'x' in d.keys():
13 w.write('found')
14print(''.join(w.data))
--- orig.py 2026-10-17 16:33:32.250802483 -0400 +++ fixed.py 2026-10-17 16:33:32.418802746 -0400 @@ -9,6 +9,6 @@ pass with Writer() as w: d = {'x': 1} - if 'x' in d.keys(): + if 'x' in d: w.write('found') print(''.join(w.data))
found
r1-p003 · Property with side effects on .keys() access
1class C:
2 def __init__(self):
3 self._count = 0
4 @property
5 def d(self):
6 self._count += 1
7 return {'a': 1}
8c = C()
9print('First access:', end=' ')
10if 'a' in c.d.keys():
11 print(c._count, end=' ')
12print('\nSecond access:', end=' ')
13if 'a' in c.d.keys():
14 print(c._count)
First access: 1 Second access: 2
r1-p004 · Runtime type annotation with typing import
1from typing import Dict
2D: Dict[str, int] = {'x': 42}
3if 'x' in D.keys():
4 print(D['x'])
--- orig.py 2026-10-17 16:33:32.692803176 -0400 +++ fixed.py 2026-10-17 16:33:32.869803453 -0400 @@ -1,4 +1,4 @@ from typing import Dict D: Dict[str, int] = {'x': 42} -if 'x' in D.keys(): +if 'x' in D: print(D['x'])
42
r1-p005 · Property returning dict-like with different behavior
1class P:
2 def __init__(self):
3 self._state = 0
4 @property
5 def mapping(self):
6 self._state ^= 1
7 return {str(self._state): self._state}
8p = P()
9print('First check:', end=' ')
10if '0' in p.mapping.keys():
11 print(p._state, end=' ')
12print('\nSecond check:', end=' ')
13if '0' in p.mapping.keys():
14 print(p._state)
First check: Second check: 0
r1-p006 · Keyword-only and positional-only args with default evaluation (generated program does not parse: Traceback (most recent call last):)
1def f(/, a, *, b=None):
2 d = {'k': 1}
3 if b is None:
4 b = []
5 if 'k' in d.keys():
6 b.append(d['k'])
7 return b
8print(f(1))
9print(f(1))
r1-p007 · Misapplication to list (no .keys()) - should not fire
1lst = [1, 2, 3]
2try:
3 # This line should NOT trigger SIM118 because lists have no .keys()
4 if 'a' in lst.keys():
5 pass
6except AttributeError as e:
7 print(type(e).__name__, str(e))
AttributeError 'list' object has no attribute 'keys'
r1-p008 · String formatting with nested quotes and f-strings
1d = {'name': 'Alice', 'age': 30}
2template = "Hello {name}, you are {age} years old"
3if 'name' in d.keys():
4 result = template.format(**d)
5else:
6 result = 'missing'
7print(result)
8# Also test f-string interaction
9if 'age' in d.keys():
10 print(f"Age: {d['age']}")
--- orig.py 2026-10-17 16:33:33.464804387 -0400 +++ fixed.py 2026-10-17 16:33:33.628804645 -0400 @@ -1,10 +1,10 @@ d = {'name': 'Alice', 'age': 30} template = "Hello {name}, you are {age} years old" -if 'name' in d.keys(): +if 'name' in d: result = template.format(**d) else: result = 'missing' print(result) # Also test f-string interaction -if 'age' in d.keys(): +if 'age' in d: print(f"Age: {d['age']}")
Hello Alice, you are 30 years old Age: 30
r2-p001 · Side effect in keys() method
1class DictWithSideEffect:
2 def __init__(self):
3 self.data = {'a': 1}
4 self.calls = 0
5 def keys(self):
6 self.calls += 1
7 print(f'keys called {self.calls}')
8 return super().keys()
9
10obj = DictWithSideEffect()
11if 'a' in obj.keys():
12 pass
13print(f'Total keys calls: {obj.calls}')
keys called 1
r2-p002 · String formatting with nested quotes and f-strings
1d = {'key': 'value'}
2if 'key' in d.keys():
3 result = f"Nested: {'inner' if 'key' in d.keys() else 'outer'}"
4else:
5 result = "not found"
6print(result)
--- orig.py 2026-10-17 16:33:38.922813027 -0400 +++ fixed.py 2026-10-17 16:33:39.089813294 -0400 @@ -1,6 +1,6 @@ d = {'key': 'value'} -if 'key' in d.keys(): - result = f"Nested: {'inner' if 'key' in d.keys() else 'outer'}" +if 'key' in d: + result = f"Nested: {'inner' if 'key' in d else 'outer'}" else: result = "not found" print(result)
Nested: inner
r2-p003 · Built-in type without keys()
1lst = [1, 2, 3]
2try:
3 if 'a' in lst.keys():
4 pass
5except AttributeError as e:
6 print(f'{type(e).__name__}: {e}')
AttributeError: 'list' object has no attribute 'keys'
r2-p004 · Star-imports and module-level side effects
1import sys
2mod = sys.modules[__name__]
3mod.shared_state = 0
4
5def update_state():
6 mod.shared_state += 1
7 return mod.shared_state
8
9class FakeDict:
10 def keys(self):
11 update_state()
12 return {'x': 1}.keys()
13
14fd = FakeDict()
15if 'x' in fd.keys():
16 pass
17print(f'Shared state: {mod.shared_state}')
Shared state: 1
r2-p005 · Class attributes vs instance attributes, descriptors, properties with side effects
1class Meta:
2 call_count = 0
3 def __get__(self, instance, owner):
4 Meta.call_count += 1
5 return {'a': 1}.keys()
6
7class Container:
8 keys = Meta()
9
10c = Container()
11if 'a' in c.keys:
12 pass
13print(f'Descriptor calls: {Meta.call_count}')
Descriptor calls: 1
r2-p006 · Keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once (generated program does not parse: Traceback (most recent call last):)
1def process(*, mapping={'a': 1}, /, **kw):
2 mapping['b'] = 2
3 return 'b' in mapping.keys()
4
5print(process())
6print(process()) # Second call sees mutated default
r2-p007 · Short-circuiting and conditional evaluation of obj.keys()
1obj = None
2safe_dict = {'key': 'value'}
3result = obj is not None and 'key' in obj.keys() or 'key' in safe_dict.keys()
4print(f'Result: {result}')
--- orig.py 2026-10-17 16:33:39.813814450 -0400 +++ fixed.py 2026-10-17 16:33:39.983814721 -0400 @@ -1,4 +1,4 @@ obj = None safe_dict = {'key': 'value'} -result = obj is not None and 'key' in obj.keys() or 'key' in safe_dict.keys() +result = obj is not None and 'key' in obj.keys() or 'key' in safe_dict print(f'Result: {result}')
Result: True
r2-p008 · Mutable default arguments and aliasing after a rewrite
1def append_key(lst=None):
2 if lst is None:
3 lst = []
4 d = {'key': 'value'}
5 if 'key' in d.keys():
6 lst.append('found')
7 return lst
8
9print(append_key())
10print(append_key()) # Aliasing: same list reused
--- orig.py 2026-10-17 16:33:40.082814880 -0400 +++ fixed.py 2026-10-17 16:33:40.252815152 -0400 @@ -2,7 +2,7 @@ if lst is None: lst = [] d = {'key': 'value'} - if 'key' in d.keys(): + if 'key' in d: lst.append('found') return lst
['found'] ['found']