ruff RUF019
labelled unknown
Analysis Nemotron 3 Super
Flags redundant 'in' membership tests before dict indexing when the subsequent truthiness check makes the guard unnecessary.
Fix: Replaces patterns like 'if key in d and d[key]:' with 'if d.get(key):', removing the redundant membership test while preserving the truthiness evaluation of the value.
10 hazards
- Rewriting 'if "x" in d and d["x"]:' changes behavior when d["x"] evaluates to False-y but non-None values like 0, '', [], or False, as d.get("x") would return those same values making the condition false where the original was true due to the membership test passing.
- If d["x"] has side effects (e.g., a property getter that logs or modifies state), moving the access into d.get() alters timing and frequency of those effects compared to the guarded evaluation.
- When d is not a local variable but an expression with side effects (e.g., f()["x"]), evaluating d twice in the original vs once in the fix changes behavior if f() is not idempotent or has observable effects.
- If the dictionary d is aliased elsewhere and mutated between the membership test and the indexing in the original code, the fix's single evaluation via get() may observe a different state than the two-step process.
- The fix drops any comments attached to the 'in' operator or the bracketed access, as the AST replacement does not preserve comment nodes attached to those specific tokens.
- In cases where d["x"] raises a KeyError for missing keys (impossible for standard dicts but possible for custom mappings), the original code avoids the error via the guard while d.get("x") would not, though this is irrelevant for built-in dict semantics.
- If the value retrieved is used later in the branch (not just tested), the fix does not provide access to the retrieved value, requiring additional changes beyond the scope of this rule's autofix.
- When the condition is part of a larger boolean expression involving short-circuiting operators, replacing the subexpression may alter evaluation order relative to other side effects in the full expression.
- If the dictionary key lookup relies on __missing__ or other custom dictionary behavior that differs between explicit key checking and .get(), the semantics may diverge, though .get() on standard dicts respects __missing__.
- The fix may incorrectly fire on code where the truthiness check is not immediate (e.g., separated by other operations), but the pattern is limited to direct adjacency per the rule's design.
Findings
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r1-p003 | fix-changes-behavior | labelled safe | real · high | related | The rule replaces a key check plus direct access with dict.get, assuming get never raises. The program uses a dict subclass whose __getitem__ raises ValueError… |
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Bytes vs string keys | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p002 | silent prediction off | Aliased dict mutation during guard evaluation | If the dictionary d is aliased elsewhere and mutated between the membership test and the indexing in the original code, the fix's single evaluation via get() may observe a different state than the two |
| r1-p003 | fix-changes-behavior | Exception in indexing with finally clause | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r1-p004 | silent prediction off | Async function with await in condition | async def/await ordering, async generators |
| r1-p005 | fix ok | Mutable default argument capturing alias | mutable default arguments and aliasing after a rewrite |
| r1-p006 | silent | Custom __missing__ behavior divergence | If the dictionary key lookup relies on __missing__ or other custom dictionary behavior that differs between explicit key checking and .get(), the semantics may diverge, though .get() on standard dicts |
Sources
r1-p001 · Bytes vs string keys
1d = {'\n': b'ok', '\r\n': b'bad'}
2data = {b'\n': True, b'\r\n': False}
3for k, v in list(d.items()) + list(data.items()):
4 if isinstance(k, str) and k in d and d[k]:
5 print('str:', repr(d[k]))
6 elif isinstance(k, bytes) and k in data and data[k]:
7 print('bytes:', repr(data[k]))
str: b'ok' str: b'bad' bytes: True
r1-p002 · Aliased dict mutation during guard evaluation
1d = {'x': [1]}
2ad = d
3try:
4 if 'x' in d and len(d.pop('x')):
5 pass
6except KeyError:
7 print('KeyError caught')
8print('d now:', d)
9print('ad still has x?', 'x' in ad)
d now: {}
ad still has x? Falser1-p003 · Exception in indexing with finally clause
1class BadDict(dict):
2 def __getitem__(self, key):
3 if key == 'fail':
4 raise ValueError('boom')
5 return super().__getitem__(key)
6d = BadDict({'fail': 0})
7try:
8 if 'fail' in d and d['fail']:
9 print('yes')
10except ValueError as e:
11 print('ValueError:', e)
12finally:
13 print('finally ran')
--- orig.py 2026-10-22 13:26:13.038760317 -0400 +++ fixed.py 2026-10-22 13:26:16.646820072 -0400 @@ -5,7 +5,7 @@ return super().__getitem__(key) d = BadDict({'fail': 0}) try: - if 'fail' in d and d['fail']: + if d.get('fail'): print('yes') except ValueError as e: print('ValueError:', e)
ValueError: boom finally ran
r1-p004 · Async function with await in condition
1async def get_val(d, k):
2 await asyncio.sleep(0)
3 return d[k]
4async def main():
5 d = {'a': 1}
6 if 'a' in d and await get_val(d, 'a'):
7 print('truthy')
8 else:
9 print('falsy')
10asyncio.run(main())
r1-p005 · Mutable default argument capturing alias
1def f(d, key='x', log=[]):
2 if key in d and d[key]:
3 log.append(d[key])
4 return log
5 return []
6d = {'x': [99]}
7print(f(d))
8d['x'] = [88]
9print(f(d))
--- orig.py 2026-10-22 13:26:13.039760333 -0400 +++ fixed.py 2026-10-22 13:26:15.658803708 -0400 @@ -1,5 +1,5 @@ def f(d, key='x', log=[]): - if key in d and d[key]: + if d.get(key): log.append(d[key]) return log return []
[[99]] [[99], [88]]
r1-p006 · Custom __missing__ behavior divergence
1class SillyDict(dict):
2 def __missing__(self, key):
3 return 'default'
4 def get(self, key, default=None):
5 return 'overridden'
6d = SillyDict({'a': 1})
7print("in+get:", 'a' in d and d.get('a'))
8print("direct get:", d.get('a'))
in+get: overridden direct get: overridden