Skip to content

FURB164 fix should validate arguments and should usually be marked unsafe #19076

Open
@dscorbett

Description

@dscorbett

Summary

The fix for unnecessary-from-float (FURB164) does not validate argument lists, so it can change the program’s behavior. It should be considered unsafe by default with a few exceptions based on type inference. When the original code is invalid, the fix should be suppressed. When the fixed code is invalid, the fix should be suppressed. When both are valid and the original uses keyword arguments, the fix should remove the keywords, because the recommended constructors do not use the same keywords. Decimal.from_float has exactly one positional-only parameter. Fraction.from_decimal has exactly one positional-or-keyword parameter, dec. Fraction.from_float has exactly one positional-or-keyword parameter, f.

Behavior changes:

$ cat >furb164_1.py <<'# EOF'
from decimal import Decimal
from fractions import Fraction

try: print(Fraction.from_decimal(dec=Decimal("4.2")))
except Exception as e: print(f"{type(e).__name__}: {e}")

try: print(Fraction.from_decimal(Decimal("4.2"), 1))
except Exception as e: print(f"{type(e).__name__}: {e}")

try: print(Decimal.from_float(4.2, None))
except Exception as e: print(f"{type(e).__name__}: {e}")

try: print(Fraction.from_decimal(numerator=Decimal("4.2")))
except Exception as e: print(f"{type(e).__name__}: {e}")
# EOF

$ python furb164_1.py
21/5
TypeError: Fraction.from_decimal() takes 2 positional arguments but 3 were given
TypeError: Decimal.from_float() takes exactly one argument (2 given)
TypeError: Fraction.from_decimal() got an unexpected keyword argument 'numerator'

$ ruff --isolated check furb164_1.py --select FURB164 --preview --fix
Found 4 errors (4 fixed, 0 remaining).

$ python furb164_1.py
TypeError: Fraction.__new__() got an unexpected keyword argument 'dec'
TypeError: both arguments should be Rational instances
4.20000000000000017763568394002504646778106689453125
21/5

If the original expression raises a TypeError, the fix can make it raise a different TypeError. Although that is safe, there is probably a bug in the original code, so it is not clear what the user intended and it would be better to suppress the fix. It is still useful for the rule to raise a diagnostic. Examples:

$ cat >furb164_2.py <<'# EOF'
from decimal import Decimal
from fractions import Fraction

try: print(Decimal.from_float(f=4.2))
except Exception as e: print(f"{type(e).__name__}: {e}")

try: print(Fraction.from_decimal(Decimal("4.2"), 1))
except Exception as e: print(f"{type(e).__name__}: {e}")
# EOF

$ python furb164_2.py
TypeError: Decimal.from_float() takes no keyword arguments
TypeError: Fraction.from_decimal() takes 2 positional arguments but 3 were given

$ ruff --isolated check furb164_2.py --select FURB164 --preview --fix
Found 2 errors (2 fixed, 0 remaining).

$ python furb164_2.py
TypeError: 'f' is an invalid keyword argument for this function
TypeError: both arguments should be Rational instances

from_float and from_decimal are not redundant with the constructors: they validate the types of their inputs. The fix removes that validation, which can change behavior. Examples:

$ cat >furb164_3.py <<'# EOF'
from decimal import Decimal
from fractions import Fraction

try: print(Decimal.from_float("4.2"))
except Exception as e: print(f"{type(e).__name__}: {e}")

try: print(Fraction.from_decimal(4.2))
except Exception as e: print(f"{type(e).__name__}: {e}")

try: print(Fraction.from_float("4.2"))
except Exception as e: print(f"{type(e).__name__}: {e}")
# EOF

$ python furb164_3.py
TypeError: argument must be int or float
TypeError: Fraction.from_decimal() only takes Decimals, not 4.2 (float)
TypeError: Fraction.from_float() only takes floats, not '4.2' (str)

$ ruff --isolated check furb164_3.py --select FURB164 --preview --fix
Found 3 errors (3 fixed, 0 remaining).

$ python furb164_3.py
4.2
4728779608739021/1125899906842624
21/5

The fix should be marked unsafe except when the argument is known to be of a valid type. Decimal.from_float and Fraction.from_float accept int, bool, and float. Fraction.from_decimal accepts int, bool, and Decimal. The fix is also safe when it simplifies a non-finite float as in Decimal.from_float(float("inf")) to Decimal("inf"). Ruff’s ResolvedPythonType, typing::is_float, and typing::is_int may be useful here.

Version

ruff 0.12.1 (32c5418 2025-06-26)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfixesRelated to suggested fixes for violationshelp wantedContributions especially welcome

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions