fixhunt

ruff RUF019: fix changes program behavior

fix-changes-behaviorlabelled safereal · highrelated
Findings / 2026-10-17T21-39-23-080Z-ah8u-ruff-RUF019 / r2-p002ruff 0.17.117 → 11 lines2026-10-17 21:39 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

2. If d is not a dict but a custom mapping with __getitem__ side effects, the fix may not preserve behavior.

Program

before
1class SideEffectMap:
2 def __init__(self):
3 self.data = {'x': 1}
4 self.calls = 0
5 def __contains__(self, key):
6 return key in self.data
7 def __getitem__(self, key):
8 return self.data[key]
9m = SideEffectMap()
10if 'x' in m and m['x']:
11 print(f'second: calls={m.calls}')
after
1class SideEffectMap:
2 def __init__(self):
3 self.data = {'x': 1}
4 self.calls = 0
5 def __contains__(self, key):
6 return key in self.data
7 def __getitem__(self, key):
8 return self.data[key]
9m = SideEffectMap()
10if m.get('x'):
11 print(f'second: calls={m.calls}')

Output

stdout beforeexit 0
second: calls=0
stdout afterexit 1
(no stdout)
AttributeError: 'SideEffectMap' object has no attribute 'get'
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · high

The rule assumes the object has a `get` method like a standard dict, but the program uses a custom mapping class that implements `__contains__` and `__getitem__` without providing `get`. The rewrite replaces a working `in`/`__getitem__` protocol with a call to a missing method, causing an AttributeError and changing the exit status from 0 to 1.

Explanation Nemotron 3 Super unverified

The original condition evaluates `'x' in m`, which invokes `__contains__` to check membership without side effects; if true, it then accesses `m['x']` via `__getitem__`. The rewritten expression uses `m.get('x')`, but `SideEffectMap` lacks a `get` method, causing an `AttributeError`. Since the left operand of `and` was true (`'x'` present), the original proceeded to evaluate `m['x']`, printing the message with zero calls. The fix replaces a valid two-part test with an undefined method call, altering both control flow and error behavior. This is not semantics‑preserving because it changes what operations are performed and introduces an exception where none occurred before.

Reproduce

file
prog.py (the before program)
minimized program
r2-p002.py
evidence sheet
r2-p002.md
regression fixture
r2-p002.py the minimized program with a header stating the observed change, for the tool's test suite
python3 prog.py
ruff check --preview --isolated --select RUF019 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

RUF019 fix · RUF019 membership test dict get · RUF019 boolean condition simplification · RUF019 truthy value check optimization

  • related astral-sh/ruff/issues/14011 same rule RUF019 but different construct (general false positives vs. missing get method on custom mapping)
  • related astral-sh/ruff/issues/12953 same rule RUF019 and same contains_effect logic but different construct (f-string side effects vs. missing get method)
  • related astral-sh/ruff/issues/12285 same rule RUF019 but different construct (not operator handling vs. missing get method)

Details

program
Custom mapping with side effects in __getitem__
hazard
2. If d is not a dict but a custom mapping with __getitem__ side effects, the fix may not preserve behavior.
oracle
stdout before: second: calls=0 stdout after: exit 0 → 1 stderr before: (none) stderr after: AttributeError: 'SideEffectMap' object has no attribute 'get'
run
2026-10-17T21-39-23-080Z-ah8u-ruff-RUF019
generated program (17 lines) and the fix's diff
1class SideEffectMap:
2 def __init__(self):
3 self.data = {'x': 1}
4 self.calls = 0
5 def __contains__(self, key):
6 return key in self.data
7 def __getitem__(self, key):
8 self.calls += 1
9 if key not in self.data:
10 raise KeyError(key)
11 return self.data[key]
12m = SideEffectMap()
13if 'x' in m and m['x']:
14 print(f'first: calls={m.calls}')
15if 'x' in m and m['x']:
16 print(f'second: calls={m.calls}')
17print(f'total calls: {m.calls}')
--- orig.py	2026-10-17 17:39:45.035453042 -0400
+++ fixed.py	2026-10-17 17:39:45.204451614 -0400
@@ -10,8 +10,8 @@
             raise KeyError(key)
         return self.data[key]
 m = SideEffectMap()
-if 'x' in m and m['x']:
+if m.get('x'):
     print(f'first: calls={m.calls}')
-if 'x' in m and m['x']:
+if m.get('x'):
     print(f'second: calls={m.calls}')
 print(f'total calls: {m.calls}')
rule documentation given to the models
# unnecessary-key-check (RUF019)

Derived from the **Ruff-specific rules** linter.

Fix is always available.

## What it does
Checks for unnecessary key checks prior to accessing a dictionary.

## Why is this bad?
When working with dictionaries, the `get` can be used to access a value
without having to check if the dictionary contains the relevant key,
returning `None` if the key is not present.

## Example
```python
if "key" in dct and dct["key"]:
    ...
```

Use instead:
```python
if dct.get("key"):
    ...
```

## Fix safety
This rule's fix is marked as safe, unless the expression contains comments
or may have side effects.