fixhunt

ruff FURB110: fix changes program behavior

fix-changes-behaviorlabelled safereal · highduplicate
Findings / 2026-10-17T21-28-24-508Z-1jyv-ruff-FURB110 / r2-p007ruff 0.17.116 → 10 lines2026-10-17 21:28 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

If the condition expression is a property with side effects in its getter, the rewrite invokes it once vs potentially twice.

Program

before
1class Sensor:
2 def __init__(self):
3 self._reads = 0
4 @property
5 def reading(self):
6 self._reads += 1
7 return self._reads # 1,2,3,...
8s = Sensor()
9alarm = s.reading if s.reading else 0
10print('sensor_reads:', s._reads, 'alarm:', alarm)
after
1class Sensor:
2 def __init__(self):
3 self._reads = 0
4 @property
5 def reading(self):
6 self._reads += 1
7 return self._reads # 1,2,3,...
8s = Sensor()
9alarm = s.reading or 0
10print('sensor_reads:', s._reads, 'alarm:', alarm)

Output

stdout beforeexit 0
sensor_reads: 2 alarm: 2
stdout afterexit 0
sensor_reads: 1 alarm: 1
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · high

The rule rewrites 's.reading if s.reading else 0' to 's.reading or 0', but the property getter has a side effect (incrementing a counter). The original evaluates the condition once, while the rewritten form would evaluate it twice if truthy, but Python's 'or' short-circuits so it evaluates once—however, the original ternary evaluates the condition twice when truthy (once for condition, once for value), so the fix changes the number of side-effect executions from 2 to 1. The documentation explicitly marks this fix as unsafe when side effects are present, yet the fix was applied anyway, causing a silent change in program behavior.

Explanation Nemotron 3 Super unverified

The property `reading` increments `_reads` each time it is accessed; evaluating `s.reading` twice in the original expression causes two increments, yielding alarm = 2 and final `_reads` = 2. The rewritten form `s.reading or 0` accesses the property only once (since its value is truthy), incrementing `_reads` once, so alarm = 1 and final `_reads` = 1. Thus the transformation changes the number of property evaluations, altering observable state.

Reproduce

file
prog.py (the before program)
minimized program
r2-p007.py
evidence sheet
r2-p007.md
regression fixture
r2-p007.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 FURB110 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

FURB110 fix · FURB110 property access conditional expression · ruff ternary operator side effect optimization · FURB110 repeated attribute evaluation warning

  • duplicate astral-sh/ruff/issues/11398 same rule FURB110 rewriting a ternary conditional with side-effecting property access into an 'or' expression, changing evaluation count
  • related astral-sh/ruff/issues/8402 same rule FURB110 autofix category but different construct (pack of fuzzer errors vs property side-effect ternary)

Details

program
Property getter side effects
hazard
If the condition expression is a property with side effects in its getter, the rewrite invokes it once vs potentially twice.
oracle
stdout before: sensor_reads: 2 alarm: 2 stdout after: sensor_reads: 1 alarm: 1
run
2026-10-17T21-28-24-508Z-1jyv-ruff-FURB110
generated program (16 lines) and the fix's diff
1class Sensor:
2 def __init__(self):
3 self._reads = 0
4 @property
5 def reading(self):
6 self._reads += 1
7 return self._reads # 1,2,3,...
8s = Sensor()
9# First, access s.reading to get a truthy value (1)
10_first = s.reading # now _reads=1
11# Condition: s.reading -> property access
12alarm = s.reading if s.reading else 0
13print('sensor_reads:', s._reads, 'alarm:', alarm)
14# Original: two property gets -> _reads increases by 2 (from 1 to 3), alarm = second reading (3)
15# Fixed: one property get -> _reads increases by 1 (to 2), alarm = that value (2)
16# Output: original alarm=3, fixed alarm=2 -> different
--- orig.py	2026-10-17 17:29:27.827044170 -0400
+++ fixed.py	2026-10-17 17:29:27.988041267 -0400
@@ -9,7 +9,7 @@
 # First, access s.reading to get a truthy value (1)
 _first = s.reading  # now _reads=1
 # Condition: s.reading -> property access
-alarm = s.reading if s.reading else 0
+alarm = s.reading or 0
 print('sensor_reads:', s._reads, 'alarm:', alarm)
 # Original: two property gets -> _reads increases by 2 (from 1 to 3), alarm = second reading (3)
 # Fixed: one property get -> _reads increases by 1 (to 2), alarm = that value (2)
rule documentation given to the models
# if-exp-instead-of-or-operator (FURB110)

Derived from the **refurb** linter.

Fix is always available.

## What it does
Checks for ternary `if` expressions that can be replaced with the `or`
operator.

## Why is this bad?
Ternary `if` expressions are more verbose than `or` expressions while
providing the same functionality.

## Example
```python
x, y = 1, 2

z = x if x else y
```

Use instead:
```python
x, y = 1, 2

z = x or y
```

## Fix safety
This rule's fix is marked as unsafe in the event that the body of the
`if` expression contains side effects or comments.

For example, `foo` will be called twice in `foo() if foo() else bar()`
(assuming `foo()` returns a truthy value), but only once in
`foo() or bar()`.