fixhunt

ruff FURB157

labelled unknown
Runs / 2026-10-17T21-33-36-873Z-na24-ruff-FURB157ruff 0.17.12026-10-17 21:33 UTC8 programs × 2 round(s)16 programs, 2 fired, 2 fixed, 0 findings, 1 rejectedmodel cost $0.0046

Analysis Nemotron 3 Super

Flags redundant str() or float() calls around integer literals or special float string literals ('inf', '-inf', 'nan') passed to Decimal constructors.

Fix: Replaces str(integer_literal) with the integer_literal itself and float(string_literal) with the string_literal itself when the string literal is 'inf', '-inf', 'nan', or case variants, within Decimal() constructor arguments.

10 hazards
  • Rewriting Decimal(str(x)) to Decimal(x) changes behavior if x is not an integer literal (e.g., variable or expression), altering type or value.
  • Rewriting Decimal(float(s)) to Decimal(s) fails if s is not exactly 'inf', '-inf', or 'nan' (case-sensitive), producing invalid Decimal input.
  • The fix may remove necessary parentheses in complex expressions, altering precedence if the argument is part of a larger expression.
  • If the str() or float() call contains a comment (e.g., str(0 # comment)), removing it would delete the comment, violating fix safety conditions.
  • Using Decimal(float('inf')) vs Decimal('inf') may differ in signaling NaNs or context-dependent behavior, though value is same; but strict equivalence is not guaranteed across all contexts.
  • If the integer literal is part of a larger expression like Decimal(str(0)+str(1)), the fix would incorrectly apply only to part, breaking syntax.
  • The rule assumes the argument is a literal; if the integer is a named constant (e.g., ZERO), rewriting Decimal(str(ZERO)) to Decimal(ZERO) may change meaning if ZERO is not an int.
  • Rewriting could interfere with debugging or profiling if the str/float call was intentionally placed for breakpoint or side effect (though pure, still alters source structure).
  • In rare cases, float('inf') might raise in restricted environments, but str(int) never does—removing float call could hide intended error handling.
  • The fix does not account for Unicode or locale-specific number formatting; assuming ASCII literals only.

Programs

idresulttitlehazard
r1-p001silent prediction offdict ordering and duplicate keysRewriting Decimal(str(...)) inside dict literals changes key insertion order when duplicate keys exist due to hash collisions.
r1-p002invalidcomment inside str() callRemoving str(0 # comment) deletes the comment, violating fix safety if the comment is semantically meaningful (e.g., TODO).
r1-p003silent prediction offclass vs instance attributes with descriptorRewriting Decimal(str(5)) to Decimal(5) in a class attribute affects __get__ descriptor behavior when accessed via instance vs class.
r1-p004silentUnicode/locale number formattingAssuming ASCII-only ignores that str() of integer literals is always ASCII, but rule misapplies if user expects localized digits (though str(int) never produces them).
r1-p005silent prediction offbytes vs str encodingConfusing str() on integer with bytes conversion; rule only applies to str, but user might intend bytes via .encode().
r1-p006fix oksignaling NaN context differenceDecimal(float('inf')) vs Decimal('inf') may differ in signaling NaN generation under certain contexts, though value compares equal.
r1-p007silentinvalid float string rewriteRewriting Decimal(float(s)) to Decimal(s) fails if s is not exactly 'inf', '-inf', or 'nan' (case-sensitive), producing invalid input.
r1-p008silent prediction offevaluation order and side effectsRewriting str(0) in f(str(0), g()) changes evaluation order if g() has side effects and str(0) is removed.
r2-p001fix oksignaling NaN contextUsing Decimal(float('inf')) vs Decimal('inf') may differ in signaling NaNs or context-dependent behavior, though value is same.
r2-p002silent prediction offkeyword-only positional forwardingkeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p003silent prediction offnamed constant not intThe rule assumes the argument is a literal; if the integer is a named constant (e.g., ZERO), rewriting Decimal(str(ZERO)) to Decimal(ZERO) may change meaning if ZERO is not an int.
r2-p004silent prediction offbytes vs str encodingbytes vs str, encoding and newline handling, text vs binary modes
r2-p005silent prediction offprecedence in complex expressionThe fix may remove necessary parentheses in complex expressions, altering precedence if the argument is part of a larger expression.
r2-p006silentpathlib vs os.path behaviorpathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths
r2-p007silent prediction offgenerators vs listsgenerators vs lists, iterators consumed once, lazy vs eager evaluation
r2-p008silent prediction offexceptions finally elseexceptions raised inside the rewritten code, finally/else clauses, exception chaining

Sources

r1-p001 · dict ordering and duplicate keys
1from decimal import Decimal
2
3d = {}
4# Insert keys that collide under hash but differ as strings vs ints
5d[Decimal(str(0))] = 'first'
6d[Decimal(0)] = 'second' # This overwrites because Decimal('0') == Decimal(0)
7print(len(d))
8for k in d:
9 print(repr(k), d[k])
1
Decimal('0') second
r1-p002 · comment inside str() call (generated program does not parse: Traceback (most recent call last):)
1from decimal import Decimal
2import ast
3
4code = "str(0 # TODO: validate zero)"
5tree = ast.parse(code, mode='eval')
6print(ast.unparse(tree))
7try:
8 v = Decimal(str(0 # TODO: validate zero))
9 print('Decimal:', v)
10except Exception as e:
11 print(type(e).__name__, str(e))
r1-p003 · class vs instance attributes with descriptor
1from decimal import Decimal
2
3class IntDesc:
4 def __get__(self, obj, owner):
5 return f'desc:{self.value}'
6
7class A:
8 value = Decimal(str(5)) # Rule target
9 desc = IntDesc()
10
11print('Class access:', A.desc)
12print('Instance access:', A().desc)
13# After fix: value becomes Decimal(5), but desc still uses class.value via descriptor
14# However, if value were used in desc, it would change -- here we show attr access depends on value type
15print('A.value:', A.value, type(A.value))
r1-p004 · Unicode/locale number formatting
1from decimal import Decimal
2# str(integer) always returns ASCII digits, even if locale set
3import locale
4try:
5 locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
6except:
7 pass
8s = str(42)
9print('str(42) repr:', repr(s), 'is ascii:', s.isascii())
10print('Decimal(str(42)):', Decimal(str(42)))
11print('Decimal(42):', Decimal(42))
12print('Equal?', Decimal(str(42)) == Decimal(42))
str(42) repr: '42' is ascii: True
Decimal(str(42)): 42
Decimal(42): 42
Equal? True
r1-p005 · bytes vs str encoding
1from decimal import Decimal
2val = 42
3# User might think str(val) -> bytes, but it's str
4s = str(val)
5print('str(val):', repr(s), type(s))
6try:
7 b = s.encode('utf-8')
8 print('encoded:', b)
9except:
10 pass
11# Rule target: Decimal(str(42))
12d1 = Decimal(str(42))
13d2 = Decimal(42)
14print('d1:', d1, 'd2:', d2, 'equal:', d1 == d2)
15# If mistakenly used bytes: Decimal(b'42') would fail
16try:
17 Decimal(b'42')
18 print('bytes worked')
19except Exception as e:
20 print('bytes fail:', type(e).__name__)
str(val): '42' <class 'str'>
encoded: b'42'
d1: 42 d2: 42 equal: True
bytes fail: TypeError
r1-p006 · signaling NaN context difference
1from decimal import Decimal, getcontext
2getcontext().traps[Decimal.InvalidOperation] = 0
3# Create sNaN via operation that differs by construction
4try:
5 inf1 = Decimal(float('inf')) # via float
6 inf2 = Decimal('inf') # direct string
7 diff = inf1 - inf2
8 print('inf1 - inf2:', diff, 'is_nan:', diff.is_nan())
9 if diff.is_nan():
10 print(' signaling:', diff.is_snan())
11except Exception as e:
12 print(type(e).__name__, str(e))
13# Reset context
14getcontext().traps[Decimal.InvalidOperation] = 1
--- orig.py	2026-10-17 17:33:49.210108221 -0400
+++ fixed.py	2026-10-17 17:33:49.618103098 -0400
@@ -2,7 +2,7 @@
 getcontext().traps[Decimal.InvalidOperation] = 0
 # Create sNaN via operation that differs by construction
 try:
-    inf1 = Decimal(float('inf'))  # via float
+    inf1 = Decimal('inf')  # via float
     inf2 = Decimal('inf')         # direct string
     diff = inf1 - inf2
     print('inf1 - inf2:', diff, 'is_nan:', diff.is_nan())
r1-p007 · invalid float string rewrite
1from decimal import Decimal
2s = 'infinity' # Not canonical
3try:
4 v = Decimal(float(s))
5 print('float(s) works:', v)
6except Exception as e:
7 print('float(s) fail:', type(e).__name__, str(e))
8try:
9 v = Decimal(s) # Direct string
10 print('Decimal(s):', v)
11except Exception as e:
12 print('Decimal(s) fail:', type(e).__name__, str(e))
13# Show rule would incorrectly fire if s were 'infinity' (but it doesn't, per spec)
14print("Rule only fires on 'inf','-inf','nan' variants")
float(s) works: Infinity
Decimal(s): Infinity
Rule only fires on 'inf','-inf','nan' variants
r1-p008 · evaluation order and side effects
1from decimal import Decimal
2
3log = []
4def g():
5 log.append('g')
6 return 42
7
8# Rule target: str(0) in first arg
9result = Decimal(str(0)), g(), log
10print('log before:', log)
11# Access to force eval
12_ = result
13print('log after:', log)
14# If str(0) were removed, g() still runs once, but if code were f(g(), str(0)) and str(0) removed, still runs.
15# To show order: make first arg have side effect via str() on var with __str__ side effect
16class StrWithSide:
17 def __str__(self):
18 log.append('strcalled')
19 return '0'
20
21log.clear()
22s = StrWithSide()
23print('Before fix:', log)
24v = Decimal(str(s)) # Would trigger if s were int literal, but it's not
25print('After str(s):', log)
26print('Decimal:', v)
27# Now with literal: this is the trigger
28log.clear()
29v2 = Decimal(str(0)) # Rule target
30print('After str(0):', log)
31print('Decimal:', v2)
32# After fix: becomes Decimal(0), no __str__ call, so log empty
33# We simulate by showing what log *would* be if fix removed call
log before: ['g']
log after: ['g']
Before fix: []
After str(s): ['strcalled']
Decimal: 0
After str(0): []
Decimal: 0
r2-p001 · signaling NaN context
1import decimal
2from decimal import Decimal, getcontext, InvalidOperation
3getcontext().traps[decimal.InvalidOperation] = True
4def trap_inf():
5 try:
6 return Decimal(float('inf'))
7 except InvalidOperation as e:
8 return f'trapped: {type(e).__name__}'
9try:
10 result = trap_inf()
11 print('result:', result)
12except Exception as e:
13 print('unexpected:', type(e).__name__, str(e))
--- orig.py	2026-10-17 17:34:00.503967385 -0400
+++ fixed.py	2026-10-17 17:34:00.687965107 -0400
@@ -3,7 +3,7 @@
 getcontext().traps[decimal.InvalidOperation] = True
 def trap_inf():
     try:
-        return Decimal(float('inf'))
+        return Decimal('inf')
     except InvalidOperation as e:
         return f'trapped: {type(e).__name__}'
 try:
result: Infinity
r2-p002 · keyword-only positional forwarding
1def make_decimal(*, base=str):
2 def inner(val=base(0)):
3 return Decimal(val)
4 return inner
5f = make_decimal()
6print('first:', f())
7print('second:', f(5))
8# second call uses str(5) -> '5' -> Decimal('5'), fix would break by using 5 directly
r2-p003 · named constant not int
1ZERO = '0'
2def get_zero():
3 return ZERO
4print('before fix:', Decimal(str(get_zero())))
5# After fix: Decimal(get_zero()) -> Decimal('0') works, but if ZERO were int, str(ZERO) vs ZERO matters
6# To show hazard: make ZERO a string that looks like int but isn't
7ZERO = '0x0'
8print('after reassignment:', Decimal(str(ZERO)))
9# Fix would do Decimal(ZERO) -> invalid literal
r2-p004 · bytes vs str encoding
1data = b'hello\nworld'
2try:
3 # Rule does not fire on bytes, but if someone mistakenly uses str on int thinking it's bytes
4 num = 42
5 s = str(num) # '42' as str
6 b = s.encode() # b'42'
7 print('str then encode:', b)
8 # If fix removes str: s = num -> int, then .encode() fails
9 # Simulate risk: function expects str
10 def needs_str(x):
11 return x.encode()
12 print('needs_str(str(42)):', needs_str(str(42)))
13except AttributeError as e:
14 print('AttributeError:', e)
str then encode: b'42'
needs_str(str(42)): b'42'
r2-p005 · precedence in complex expression
1def compute():
2 return 1 + Decimal(str(2)) * 3 # str(2) -> '2' -> Decimal('2'), then *3 then +1
3# Without str: 1 + Decimal(2) * 3 -> same due to Decimal __mul__ precedence? Actually Decimal*int is allowed.
4# But: make it depend on string vs int in addition
5class Dummy:
6 def __add__(self, other):
7 return f'add({self}, {other})'
8 def __mul__(self, other):
9 return f'mul({self}, {other})'
10d = Dummy()
11print('with str:', 1 + d * 3) # d*3 first -> mul(d,3), then 1 + that -> add(1, mul(d,3))
12# If we had str(d) it would be different, but rule doesn't apply.
13# Instead: show that str() affects type in expression
14print('Decimal(str(2)) * 3:', Decimal(str(2)) * 3)
15print('Decimal(2) * 3:', Decimal(2) * 3)
16# Same value, but if we had overloaded * for str vs int...
r2-p006 · pathlib vs os.path behavior
1import os
2from pathlib import Path
3# Rule doesn't fire here, but show where str() on int is used in path context
4num = 42
5p = Path(str(num)) # creates Path('42')
6print('Path(str(42)):', p.exists())
7# Fix would do Path(42) -> TypeError: expected str, bytes or os.PathLike object, not int
8try:
9 Path(42)
10except TypeError as e:
11 print('Path(42) error:', e)
Path(str(42)): False
Path(42) error: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int'
r2-p007 · generators vs lists
1def gen():
2 yield from [str(i) for i in range(3)]
3g = gen()
4print('first list:', list(g)) # consumes generator
5print('second list:', list(g)) # empty
6# Now with fix hazard: if we had Decimal(str(next(gen()))) etc.
7# Instead: show that str() on iterator item matters
8def process():
9 return [Decimal(str(x)) for x in gen()] # gen is exhausted!
10print('process():', process())
11# If fix removes str: Decimal(x) where x is str -> works, but if x were int...
12# Make gen yield ints
first list: ['0', '1', '2']
second list: []
r2-p008 · exceptions finally else
1def risky():
2 try:
3 return Decimal(str(1 / 0)) # ZeroDivisionError in str() arg
4 except ZeroDivisionError as e:
5 raise ValueError('bad') from e
6 else:
7 return 'ok'
8 finally:
9 print('finally ran')
10try:
11 risky()
12except ValueError as e:
13 print('caught:', e)
14 print('cause:', type(e.__cause__).__name__ if e.__cause__ else None)
15# If fix removes str: Decimal(1 / 0) -> same ZeroDivisionError, but now inside Decimal()
16# Difference: if str() had side effects... but it doesn't.
17# Instead: make str() raise on certain input
18class BadStr:
19 def __str__(self):
20 raise RuntimeError('str failed')
21 __repr__ = __str__
22try:
23 Decimal(str(BadStr()))
24except RuntimeError as e:
25 print('str failed:', e)
26# Fix would do Decimal(BadStr()) -> calls __repr__ or __str__? Decimal constructor uses str() internally?
27# Actually Decimal(BadStr()) will call str(BadStr()) anyway, so same.
28# Need: where str() is not used by Decimal but we rely on it.
29# Hazard overstated; use: str() in arg that is used elsewhere
finally ran