Skip to content

Ensure FloatRange clamping always returns a float#3548

Closed
sarathfrancis90 wants to merge 2 commits into
pallets:mainfrom
sarathfrancis90:fix-floatrange-clamp-int-bound
Closed

Ensure FloatRange clamping always returns a float#3548
sarathfrancis90 wants to merge 2 commits into
pallets:mainfrom
sarathfrancis90:fix-floatrange-clamp-int-bound

Conversation

@sarathfrancis90
Copy link
Copy Markdown

FloatRange(..., clamp=True) could return an int instead of a float when an out-of-range value was clamped to a boundary that had been written as an integer literal.

import click

t = click.FloatRange(0, 1, clamp=True)
print(repr(t.convert("2.5", None, None)))   # 1   (int)  -> should be 1.0
print(repr(t.convert("-2.5", None, None)))  # 0   (int)  -> should be 0.0

FloatRange is declared as _NumberRangeBase[float] / FloatParamType and is documented to return a float, so returning an int here violates the type contract.

Root cause

The constructor stores the boundaries exactly as they were passed in, so FloatRange(0, 1, clamp=True) keeps self.min/self.max as Python ints. FloatRange._clamp returned the stored boundary verbatim for non-open bounds, propagating that int to the caller.

Fix

Coerce the boundary through self._number_class (which is float for FloatRange) before returning it, so a clamped value is always a float. In-range values and IntRange are unaffected.

Tests

Added test_float_range_clamp_returns_float, parametrized over a value above and below the range, asserting type(result) is float. The existing clamp tests use float boundary literals (0.5/1.5) and test_range compares with == (where 1 == 1.0), so they did not catch this. The new test fails on main (AssertionError: <class 'int'> is float) and passes with the change.

fixes #3547


  • Add tests that demonstrate the correct behavior of the change. Tests fail without the change.
  • Add or update relevant docs, in the docs folder and in code.
  • Add an entry in CHANGES.rst summarizing the change and linking to the issue.
  • Add .. versionchanged:: entries in relevant code docs.

FloatRange._clamp returned the stored boundary directly for non-open
bounds. The boundaries are kept exactly as passed to the constructor, so
a value written as an int literal (e.g. FloatRange(0, 1, clamp=True))
caused clamping to return an int instead of a float, breaking the
documented return type. Coerce the boundary through the number class so a
clamped value is always a float.

fixes pallets#3547
@davidism davidism closed this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FloatRange with clamp=True returns int when the boundary was given as an int

2 participants