# ruff RUF019: fix changes program behavior

Internal evidence sheet generated by fixhunt. Not an issue text. A human re-verifies, checks the duplicates
below, and writes the upstream report in their own words following the project's AI policy.

- Tool: ruff ruff 0.17.1
- Rule: RUF019 (tool labels the fix: unknown, this fix reported as safe)
- Oracle: fix-changes-behavior
- Judge (Nemotron 3 Ultra, advisory): real, severity 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.
- Program: minimized from 13 to 9 lines

## Reproducer

```python
class BadDict(dict):
    def __getitem__(self, key):
            raise ValueError('boom')
d = BadDict({'fail': 0})
try:
    if 'fail' in d and d['fail']:
        print('yes')
except ValueError as e:
    print('finally ran')
```

## Commands

```sh
python3 prog.py
ruff check --preview --isolated --select RUF019 --fix prog.py
python3 prog.py
```

## Observed

```
stdout before:
finally ran
stdout after:
```

## Diff applied by the fix

```diff
--- orig.py	2026-10-22 13:26:24.169944705 -0400
+++ fixed.py	2026-10-22 13:26:27.149994089 -0400
@@ -3,7 +3,7 @@
             raise ValueError('boom')
 d = BadDict({'fail': 0})
 try:
-    if 'fail' in d and d['fail']:
+    if d.get('fail'):
         print('yes')
 except ValueError as e:
     print('finally ran')
```

## Why the rewrite is not equivalent (model draft, verify before use)

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.

## Upstream search

Queries: "RUF019 fix", "RUF019 dict get truthiness check", "RUF019 replace membership test with get", "RUF019 avoid KeyError via safe access"

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

<details><summary>All hits</summary>

- [github, open] Possible false positives for RUF019 — https://github.com/astral-sh/ruff/issues/14011
- [github, closed] unused-function-argument not silenced by prefixed underscore if variable name is "次" — https://github.com/astral-sh/ruff/issues/28721
- [github, closed] `contains_effect` should consider f-strings to have effects — https://github.com/astral-sh/ruff/issues/12953
- [github, closed] False negative for RUF019 with `not` operator — https://github.com/astral-sh/ruff/issues/12285
- [github, closed] Fixed code unformatted — https://github.com/astral-sh/ruff/issues/8619
- [github, closed] Add option to disable preview warnings — https://github.com/astral-sh/ruff/issues/8620

</details>
