Conditionals and Rounding with `st.number_input()`

I have two st.number_inputs() I am trying to use

input1 = st.number_input("x", value=5.0, step=0.5, format="%0.1f")

input2 = st.number_input("y", value=-110)

They work great, except for a few specific cases. As for input1, I’d like for it to always be rounded to the nearest half number. The step works, but when a float such as 5.4 is given, the number is not rounded and the step behaves as normal and it goes from 5.4, 5.9, etc. when I’d like the 5.4 to be rounded to 5.5 and behave like 5.5, 6.0, etc… Is there a way to always round any given number to the nearest 0.5?

As for input2, I’d like to start counting the opposite direction once the number gets above -100. ie, instead of -102, -101, -100, -99, I’d like for it to go -102, -101, -100, +101, +102, … etc. Is it possible to add conditionals like this to an st.number_input()? Furthermore, If any number between -100 and +100 is given, I’d like to round it (similar to input1) to either -100 or +100, whichever is closer.

So basically, my question boils down to two main topics: is it possible to round a number given to st.number_input() and add certain conditionals?

No, this behavior isn’t possible to add directly to number_input, but you could always add some logic after the st.number_input’s to round input1 and input2 however you’d like.

You can use a callback to round on_change.

import streamlit as st

def forced_round():
    st.session_state.half = round(st.session_state.half*2)/2

st.number_input('Half Steps',0.0,10.0, step=.5, key='half', on_change=forced_round)



def skip_100(previous):
    if previous == 100 and st.session_state.zone == 99:
        st.session_state.zone = -100
    elif previous == -100 and st.session_state.zone == -99:
        st.session_state.zone = 100
    elif 0 <= st.session_state.zone < 100:
        st.session_state.zone = 100
    elif -100 < st.session_state.zone < 0:
        st.session_state.zone = -100

if 'zone' not in st.session_state:
    st.session_state.zone = 100

st.number_input('No Go Zone of 100',-1000,1000, step=1, key='zone', on_change=skip_100, args=[st.session_state.zone])

The only thing is that it can’t distinguish between a manually entered change or one from clicking the increment button. As such, manually typing 99 when it currently says 100 will cause it to jump down to -100. With any other current value, entering 99 will round up to 100.

1 Like

Ugh, this seems like a great solution but I have these widgets in a form so I get the following error

StreamlitAPIException: With forms, callbacks can only be defined on the st.form_submit_button. Defining callbacks on other widgets inside a form is not allowed.

Is there a way around this? Can I declare these callbacks within the form some other way?

If you have widgets in a form, any callback functionality would have to be tied to the submit button and would then only execute upon form submission. That creates a bit of difficulty with your second widget especially. Clicking increment button would unlikely produce the desired result since you wouldn’t be tracking the state of the widget at the time of each click.

Can you forgo the form and use other logic/caching to make sure it doesn’t impact performance? Generally, custom widget logic/widget interactivity is difficult within forms.

1 Like

I have a bunch of widgets in the form that end up filtering a data frame (these are just two of them that have very specific requirements), so it is ideal for them to all get submitted at once; it would be difficult for me to get rid of the form, I’m afraid. That is too bad, I will just set some error handling for the 100 no go zone and do the rounding after the form has been submitted, but I was hoping there was a way I could do this visually for the end user as well in real time before they submit the form just for the aesthetic appeal. Nonetheless, I appreciate your help and creative solution!

Even if you have a hundred widgets, you can usually create the logic to have a “form effect” and hold off on other processing until a button is clicked.

Imagine if have 100 widgets with keys w_1, w_2, … w_100. Then imagine a submit button that copies each value at w_i to use_i with your script utilizing the copied values. A user would be free to change any number of widgets without impacting the result (until they clicked submit).

This may be problematic if you have certain kinds of “heavy” objects displayed, but is often doable one way or another.

1 Like

Interesting… Is there a reference for building something with this type of logic? I’d like to check it out and see if it’s worth working in and using instead of a nice, clean form just to get the aesthetic changes for these two specific widgets. The data frame being filtered is not very computationally expensive, and there can be up to ~20 widgets… so nothing crazy at all.

Here’s a simple example:

import streamlit as st
import pandas as pd

if 'df' not in st.session_state:
    st.session_state.df = pd.DataFrame({'A':[1]*5 + [2]*5 + [3]*5 + [4]*5 + [5]*5,
                                        'B':[1,2,3,4,5]*5})
df = st.session_state.df

A = st.number_input('A',1,5,step=1, key='_A')
B = st.number_input('B',1,5,step=1, key='_B')

def submit():
    st.session_state.A = st.session_state._A
    st.session_state.B = st.session_state._B

st.button('Submit', on_click=submit)

if 'A' in st.session_state:
    st.write(df[(df['A']==st.session_state.A)&(df['B']==st.session_state.B)])

1 Like

Thanks so much! Going to give this a go.