Repository: streamlit/streamlit
File: lib/streamlit/elements/widgets/number_input.py (Python)

Code before:


@dataclass
class NumberInputSerde:
    value: Number | None
    data_type: int
    min_value: Number
    max_value: Number

    def serialize(self, v: Number | None) -> Number | None:
        return v

    def deserialize(self, ui_value: Number | None) -> Number | None:
        val: Number | None = ui_value if ui_value is not None else self.value

        if val is not None:
            if self.data_type == NumberInputProto.INT:
                val = int(val)
            # Clamp to [min_value, max_value]. Primarily needed for
            # out-of-range values seeded from URL query params; a no-op
            # for frontend values since the UI already enforces bounds.
            # Note: This only runs on *serialized* values (URL seeding,
            # fresh frontend submissions). Already-deserialized values
            # from previous runs are handled by the bounds-reset check
            # in _number_input (see "Validate the current value" block).
            val = max(self.min_value, min(self.max_value, val))

        return val


class NumberInputMixin:
    # If "min_value: int" is given and all other numerical inputs are
    #   "int"s or not provided (value optionally being "min"), return "int"
    # If "min_value: int, value: None" is given and all other numerical inputs
    #   are "int"s or not provided, return "int | None"
    @overload
    def number_input(
        self,
        label: str,
        min_value: int,
        max_value: int | None = None,
        value: IntOrNone | Literal["min"] = "min",
        step: int | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | None = None,
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,
        placeholder: str | None = None,

Review comment (suggestion):
We already have a similar mechanism in number_input to handle out-of-range values e.g., when users set it via `st.session_state` or when min / max dynamically change when `key` is set. In those cases, the session_state, return value and frontend value just gets reset to the default. E.g. you can try it here:

```python
import streamlit as st

if st.button("Set out of range value"):
    st.session_state.number_input = 101

if st.button("Set valid value"):
    st.session_state.number_input = 10


st.number_input("Number input", value=5, min_value=0, max_value=100, key="number_input")
st.write(st.session_state)
```

This might work out of the box here as well, not sure. But it might be good to double-check whether you want a different behaviour when set via query parameters (cc @jrieke). 

Code after:


@dataclass
class NumberInputSerde:
    value: Number | None
    data_type: int
    min_value: Number
    max_value: Number

    def serialize(self, v: Number | None) -> Number | None:
        return v

    def deserialize(self, ui_value: Number | None) -> Number | None:
        val: Number | None = ui_value if ui_value is not None else self.value

        if val is not None:
            if self.data_type == NumberInputProto.INT:
                val = int(val)
            # Reset to default if outside [min_value, max_value]. This
            # rejects out-of-range values seeded from URL query params;
            # a no-op for frontend values since the UI enforces bounds.
            # Returning the default triggers _seed_widget_from_url's
            # "deserialized == default" check, which clears the URL param.
            if val < self.min_value or val > self.max_value:
                return self.value

        return val


class NumberInputMixin:
    # If "min_value: int" is given and all other numerical inputs are
    #   "int"s or not provided (value optionally being "min"), return "int"
    # If "min_value: int, value: None" is given and all other numerical inputs
    #   are "int"s or not provided, return "int | None"
    @overload
    def number_input(
        self,
        label: str,
        min_value: int,
        max_value: int | None = None,
        value: IntOrNone | Literal["min"] = "min",
        step: int | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | None = None,
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,
        placeholder: str | None = None,
        disabled: bool = False,