fixhunt

ruff SIM118

labelled unknown
Runs / 2026-10-17T20-33-23-506Z-8cn9-ruff-SIM118ruff 0.17.12026-10-17 20:33 UTC8 programs × 2 round(s)16 programs, 13 fired, 6 fixed, 0 findings, 2 rejectedmodel cost $0.0031

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

idresulttitlehazard
r1-p001fired, no fixCustom class with overridden __contains__If obj is a custom class where __contains__ differs from membership in .keys() (violating dict contract), semantics change.
r1-p002fix okContext manager without close() but with write()context managers that are not files, objects with write() but no close()
r1-p003fired, no fixProperty with side effects on .keys() accessclass attributes vs instance attributes, descriptors, properties with side effects
r1-p004fix ok prediction offRuntime type annotation with typing importtype annotations evaluated at runtime, typing-only imports
r1-p005fired, no fixProperty returning dict-like with different behaviorIf 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-p006invalidKeyword-only and positional-only args with default evaluationkeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r1-p007fired, no fix prediction offMisapplication to list (no .keys()) - should not fireIf 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-p008fix okString formatting with nested quotes and f-stringsstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p001fired, no fixSide effect in keys() methodIf obj.keys() has side effects (e.g., prints, state changes), removing the call eliminates those effects.
r2-p002fix okString formatting with nested quotes and f-stringsstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p003fired, no fix prediction offBuilt-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-p004fired, no fixStar-imports and module-level side effectsstar-imports, module-level side effects, __all__
r2-p005silent prediction offClass attributes vs instance attributes, descriptors, properties with side effectsclass attributes vs instance attributes, descriptors, properties with side effects
r2-p006invalidKeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated oncekeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p007fix okShort-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-p008fix okMutable default arguments and aliasing after a rewritemutable 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']