ruff RUF032
labelled unknown
Analysis Nemotron 3 Super
Flags Decimal() constructor calls where the argument is a floating-point literal.
Fix: Replace the floating-point literal argument with its exact string representation using repr() to preserve the original float's value.
12 hazards
- If the float argument is a variable (e.g., Decimal(x)), the fix incorrectly replaces it with a string literal, altering semantics and potentially causing TypeError.
- If the float argument is an expression (e.g., Decimal(1.0 + 2.0)), the fix replaces the entire expression with a string, changing runtime behavior.
- If the float is a special value like float('nan') or float('inf'), repr() produces strings like 'nan' or 'inf', which Decimal() accepts but may propagate differently in downstream calculations.
- If the code relies on the exact binary approximation of the float (e.g., for bit-level reproduction), converting to string then to Decimal yields a different rational number than the original float-to-Decimal conversion.
- If the Decimal call is inside a larger expression (e.g., Decimal(1.2) + 3), the fix only affects the constructor argument, but surrounding code may still behave differently due to altered precision.
- If the float literal is part of a tuple or list unpacked via *args (e.g., Decimal(*[1.2])), the fix does not apply and Ruff may not flag it, but if it did, rewriting would break unpacking.
- If the code uses a custom Decimal subclass that overrides __init__ to expect a float, replacing with a string breaks initialization.
- If the float literal appears in a comment or docstring near the call (e.g., # value: 1.2), the fix does not touch comments, but the semantic change may invalidate the comment's accuracy.
- If the float is the result of a function call with side effects (e.g., Decimal(get_price())), the fix would incorrectly replace the call with a string, eliminating side effects.
- If the code runs under a strict typing regime (e.g., mypy) that expects Decimal(float), the rewritten Decimal(str) may trigger false-positive type errors.
- If the float literal is syntactically identical to an integer literal but typed as float (e.g., Decimal(2.0)), the fix produces '2.0', which is semantically equivalent but may affect code generation tools that distinguish float vs int origins.
- If the code is parsed by a tool that relies on the AST node type being a constant float, replacing it with a string constant alters the AST and may break downstream tooling.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Variable as float argument | If the float argument is a variable (e.g., Decimal(x)), the fix incorrectly replaces it with a string literal, altering semantics and potentially causing TypeError. |
| r1-p002 | silent prediction off | Expression as float argument | If the float argument is an expression (e.g., Decimal(1.0 + 2.0)), the fix replaces the entire expression with a string, changing runtime behavior. |
| r1-p003 | silent prediction off | Special float values (inf, nan) | If the float is a special value like float('nan') or float('inf'), repr() produces strings like 'nan' or 'inf', which Decimal() accepts but may propagate differently in downstream calculations. |
| r1-p004 | silent prediction off | Int/float/bool coercion and arithmetic | int/float/bool coercion, Decimal, division and rounding |
| r1-p005 | silent prediction off | Keyword-only and defaults | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p006 | silent prediction off | Function call with side effects | If the float is the result of a function call with side effects (e.g., Decimal(get_price())), the fix would incorrectly replace the call with a string, eliminating side effects. |
| r1-p007 | silent | Context managers (non-file) | context managers that are not files, objects with write() but no close() |
| r1-p008 | silent | pathlib vs os.path | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r2-p001 | silent prediction off | Expression as float argument | Replaces entire expression with string, changing runtime behavior. |
| r2-p002 | fired, no fix | AST node type changed from constant float to string constant | Breaks downstream tooling that inspects AST nodes expecting float constants. |
| r2-p003 | silent prediction off | String formatting edge cases: % formatting, f-strings, repr vs str | Alters formatting behavior when the Decimal is used in string operations. |
| r2-p004 | silent prediction off | Dict ordering, duplicate keys, __hash__/__eq__ overrides, sets | Changes hash/equality behavior affecting dict/set membership and ordering. |
| r2-p005 | silent prediction off | Type annotations evaluated at runtime, typing-only imports | Alters runtime evaluation of type annotations involving the Decimal. |
| r2-p006 | silent prediction off | Special float values: inf, nan | repr() produces 'inf'/'nan' strings which Decimal() accepts but may propagate differently. |
| r2-p007 | silent prediction off | File modes and truncation, partial writes, non-str data passed to write | Changes behavior when writing Decimal to file due to altered __str__ or type. |
| r2-p008 | silent prediction off | Exceptions raised inside rewritten code, finally/else clauses, exception chaining | Alters exception flow when Decimal construction is used in try-except-finally. |
Sources
r1-p001 · Variable as float argument
1x = 1.5
2print(type(Decimal(x)))
3# After fix: Decimal(str(1.5)) -> Decimal('1.5')
r1-p002 · Expression as float argument
1result = Decimal(1.0 + 2.0)
2print(result)
3# After fix: Decimal('3.0') instead of Decimal(3.0) -> same value but different internal handling; test via equality with string-made Decimal
4print(result == Decimal('3.0'))
r1-p003 · Special float values (inf, nan)
1import math
2inf_val = float('inf')
3an_val = float('nan')
4dec_inf = Decimal(inf_val)
5dec_nan = Decimal(nan_val)
6print(f'inf comparison: {dec_inf == Decimal("inf")}')
7print(f'nan comparison: {dec_nan != dec_nan}') # NaN != NaN
8# After fix: Decimal('inf'), Decimal('nan') -> same behavior? But test if operations differ
9print(f'inf + 1: {dec_inf + 1}')
10print(f'nan + 1: {dec_nan + 1}')
r1-p004 · Int/float/bool coercion and arithmetic
1a = Decimal(1.0) # triggers
2b = Decimal(2) # does not trigger (int)
3c = Decimal(True) # does not trigger (bool, but 1.0 -> might trigger? bool is subclass of int, but Decimal(True) is allowed)
4# Actually, Decimal(True) is valid and equals Decimal(1). Rule does not fire on bool because it's not a float literal.
5# So adjust: use 1.0 * True -> still 1.0
6val = 1.0 * True
7d = Decimal(val) # triggers because val is float 1.0
8print(f'a: {a}, b: {b}, c: {c}, d: {d}')
9print(f'a == d: {a == d}') # Should be True
10# After fix: d becomes Decimal('1.0') -> same value
11# Test rounding context
12from decimal import getcontext
13getcontext().prec = 2
14e = Decimal(1.0 / 3.0) # triggers
15print(f'e: {e}')
16# After fix: Decimal(str(1.0/3.0)) -> Decimal('0.3333333333333333') vs original Decimal(0.3333333333333333) -> same
r1-p005 · Keyword-only and defaults
1def func(x, *, y=1.0): # y default is float
2 return Decimal(x) + Decimal(y)
3print(func(2)) # uses y=1.0 -> triggers on Decimal(y)
4# After fix: Decimal(str(1.0)) -> Decimal('1.0')
5# But note: default is evaluated once at definition, so replacement would be in the function's bytecode?
6# However, the fix would change the source: Decimal(y) -> Decimal(str(1.0)) only if y is a float literal, but y is a variable.
7# Wait, the rule fires on Decimal(y) only if y is a float literal, which it's not.
8# So this does not trigger. Need to adjust.
9# Instead, make the call have a float literal in keyword argument:
10print(func(2, y=1.0)) # now Decimal(1.0) in keyword argument -> triggers
r1-p006 · Function call with side effects
1side_effect_log = []
2def get_price():
3 side_effect_log.append('called')
4 return 99.9
5try:
6 d = Decimal(get_price()) # triggers
7 print(f'Decimal: {d}, log: {side_effect_log}')
8except Exception as e:
9 print(f'{type(e).__name__}: {e}')
10# After fix: Decimal(str(99.9)) -> side effect log remains empty
NameError: name 'Decimal' is not defined
r1-p007 · Context managers (non-file)
1class LogCtx:
2 def __enter__(self):
3 self.log = []
4 return self
5 def __exit__(self, *exc):
6 pass
7 def write(self, s):
8 self.log.append(s)
9with LogCtx() as ctx:
10 ctx.write('start')
11 x = 1.5
12 d = Decimal(x) # does not trigger because x is variable
13 ctx.write(f'd={d}')
14print(''.join(ctx.log))
r1-p008 · pathlib vs os.path
1import os
2import pathlib
3# Create a temporary file
4pathlib.Path('test.txt').write_text('hello')
5try:
6 # These do not involve Decimal, so rule does not fire
7 p1 = pathlib.Path('test.txt')
8 p2 = os.path.path('test.txt') # AttributeError: module 'os' has no attribute 'path'
9except AttributeError as e:
10 print(f'{type(e).__name__}: {e}')
11finally:
12 try:
13 os.remove('test.txt')
14 except FileNotFoundError:
15 pass
AttributeError: module 'posixpath' has no attribute 'path'
r2-p001 · Expression as float argument
1from decimal import Decimal
2x = 1.0
3y = 2.0
4z = Decimal(x + y)
5print(z)
3
r2-p002 · AST node type changed from constant float to string constant
1import ast
2from decimal import Decimal
3node = ast.parse('Decimal(1.5)', mode='eval')
4const_val = node.body.value
5if isinstance(const_val, ast.Constant):
6 print(f'AST const type: {type(const_val.value)}, value: {const_val.value}')
7else:
8 print('Not a Constant')
9d = Decimal(1.5)
10print(f'Decimal value: {d}')
r2-p003 · String formatting edge cases: % formatting, f-strings, repr vs str
1from decimal import Decimal
2val = Decimal(1.0 + 2.0)
3s1 = '%s' % val
4s2 = f'{val}'
5s3 = str(val)
6s4 = repr(val)
7print(f'%s: {s1!r}, f-string: {s2!r}, str(): {s3!r}, repr(): {s4!r}')
%s: '3', f-string: '3', str(): '3', repr(): "Decimal('3')"r2-p004 · Dict ordering, duplicate keys, __hash__/__eq__ overrides, sets
1from decimal import Decimal
2d = {}
3d[Decimal(1.0 + 2.0)] = 'first'
4d[Decimal('3.0')] = 'second'
5print(f'Dict length: {len(d)}')
6for k, v in d.items():
7 print(f'key: {k!r} ({type(k)}), value: {v!r}')
8s = {Decimal(1.0 + 2.0), Decimal('3.0')}
9print(f'Set length: {len(s)}')
Dict length: 1
key: Decimal('3') (<class 'decimal.Decimal'>), value: 'second'
Set length: 1r2-p005 · Type annotations evaluated at runtime, typing-only imports
1from decimal import Decimal
2from typing import get_type_hints
3def func() -> Decimal:
4 return Decimal(1.0 + 2.0)
5hints = get_type_hints(func)
6print(f'Return type hint: {hints.get("return")}')
7val = func()
8print(f'Returned value: {val}')
Return type hint: <class 'decimal.Decimal'> Returned value: 3
r2-p006 · Special float values: inf, nan
1from decimal import Decimal, InvalidOperation
2try:
3 d1 = Decimal(float('inf'))
4 print(f'inf Decimal: {d1}')
5except Exception as e:
6 print(f'inf error: {type(e).__name__}: {e}')
7try:
8 d2 = Decimal(float('nan'))
9 print(f'nan Decimal: {d2}')
10except Exception as e:
11 print(f'nan error: {type(e).__name__}: {e}')
12print(f'Decimal("Inf"): {Decimal("Inf")}')
13print(f'Decimal("NaN"): {Decimal("NaN")}')
inf Decimal: Infinity
nan Decimal: NaN
Decimal("Inf"): Infinity
Decimal("NaN"): NaNr2-p007 · File modes and truncation, partial writes, non-str data passed to write
1from decimal import Decimal
2import os
3filename = 'test_output.txt'
4if os.path.exists(filename):
5 os.remove(filename)
6with open(filename, 'w') as f:
7 f.write(str(Decimal(1.0 + 2.0)))
8with open(filename, 'r') as f:
9 content = f.read()
10print(f'File content: {content!r}')
11if os.path.exists(filename):
12 os.remove(filename)
File content: '3'
r2-p008 · Exceptions raised inside rewritten code, finally/else clauses, exception chaining
1from decimal import Decimal
2try:
3 result = Decimal(1.0 + 2.0)
4 print(f'Success: {result}')
5except Exception as e:
6 print(f'Exception: {type(e).__name__}: {e}')
7else:
8 print('Else block executed')
9finally:
10 print('Finally block executed')
Success: 3 Else block executed Finally block executed