fixhunt

ruff RUF019: fix changes program behavior

fix-changes-behaviorlabelled safereal · highrelated
Findings / 2026-10-22T17-26-00-716Z-5zvc-ruff-RUF019 / r1-p003ruff 0.17.113 → 9 lines2026-10-22 17:26 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

exceptions raised inside the rewritten code, finally/else clauses, exception chaining

Program

before
1class BadDict(dict):
2 def __getitem__(self, key):
3 raise ValueError('boom')
4d = BadDict({'fail': 0})
5try:
6 if 'fail' in d and d['fail']:
7 print('yes')
8except ValueError as e:
9 print('finally ran')
after
1class BadDict(dict):
2 def __getitem__(self, key):
3 raise ValueError('boom')
4d = BadDict({'fail': 0})
5try:
6 if d.get('fail'):
7 print('yes')
8except ValueError as e:
9 print('finally ran')

Output

stdout beforeexit 0
finally ran
stdout afterexit 0
(no stdout)
fired
1 before · 0 after
runs
each version twice, same file name, no network
minimizer
44 oracle evaluations

Judge Nemotron 3 Ultra advisory real · high

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; the original code catches that exception, but the rewritten code calls get (which inherits the raising __getitem__) and the exception propagates out, changing program output. The documentation presents the rewrite as equivalent and marks the fix safe, so this is a real bug in the fix.

Explanation Nemotron 3 Super unverified

The original condition checks membership with `'fail' in d` first; since `__getitem__` raises `ValueError` only on access, the membership test succeeds without triggering the exception, so the second part `d['fail']` is evaluated, raising `ValueError`, which is caught and prints 'finally ran'. The rewritten condition uses `d.get('fail')`, which directly invokes `__getitem__` for the key 'fail', immediately raising `ValueError` before any short-circuiting can occur. This exception is still caught, but the `print('yes')` is never reached because the exception happens during condition evaluation, not after it. Thus, the output changes from 'finally ran' to nothing.

Reproduce

file
prog.py (the before program)
minimized program
r1-p003.py
evidence sheet
r1-p003.md
regression fixture
r1-p003.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 dict get truthiness check · RUF019 replace membership test with get · RUF019 avoid KeyError via safe access

  • related astral-sh/ruff/issues/14011 same rule RUF019 but different construct (general false positives vs custom __getitem__ side effects)
  • related astral-sh/ruff/issues/12953 same rule RUF019 and same contains_effect logic but different construct (f-strings vs custom dict __getitem__)
  • related astral-sh/ruff/issues/12285 same rule RUF019 but different construct (not operator vs and operator with side effects)

Details

program
Exception in indexing with finally clause
hazard
exceptions raised inside the rewritten code, finally/else clauses, exception chaining
oracle
stdout before: finally ran stdout after:
run
2026-10-22T17-26-00-716Z-5zvc-ruff-RUF019
generated program (13 lines) and the fix's diff
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)
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.