ruff RUF019: fix changes program behavior
Hypothesis Nemotron 3 Super predicted to fire fired 1×
exceptions raised inside the rewritten code, finally/else clauses, exception chaining
Program
Output
finally ran
(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
--- 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.