How to use the "key" field in interactive widgets API

All interactive widgets have “key” as the last argument in the argument list. Is there a way to get the key of the widget which triggered the rerun? Are there examples explaining the use cases for this argument?

Here is the use case I would like to explore to avoid a rerun of the entire script whenever a widget value is modified:

Let us think of a “form” with three widgets - two input widgets (“Lower Threshold”, “Upper Threshold”) and a “Submit” button. The key argument of these two input widgets will have na_ (no action …) as a prefix and the submit button will have ta_ (take action …) as a prefix. During the rerun, these prefixes will be checked and used to control the flow.

3 Likes

Hi @Rajesh_Kumar

I would also like to have support for forms where the script is only rerun on a “submit” button or similar.

1 Like

Hi @Marc ,
Having a support for forms will be the ideal solution. In the meantime I am looking for a workaround. Any suggestion, other than local caching, is welcome. My application requires data extraction from an external url. I would like to avoid fetching the same data again and again from an external resource due to rerun triggered by changes in widget values which are used for data cleaning after data extraction.

Use of the key associated with the last widget used, if available, could be an attempt towards this workaround.

I am having almost exactly the same issue, I am searching an external api (which takes quite some time) and I only want to run it when I hit the ‘search’ button. But when I try to implement this it works for displaying the results on hitting the button, but then when I do anything with the results and the script is rerun I lose the results.

Could you elaborate the workaround that you had in mind?

Hi @Rajesh_Kumar,
thanks for posting on the forum. The key field purpose is used to disambiguate among widgets with the same label. We use the label to uniquely identify a widget, so, if they have the same label, a user defined key is used to disambiguate. Given that a widget returns a value, it does not expose the key through the API.

Nevertheless, the use-case you are mentioning is really interesting. We don’t have a way not to re-run scripts when widget state change and we suggest using st.cache to cache data from sources. You are also more than welcome to open a Feature Request around this here, we do consider those during our planning and add them to the roadmap.

Best,
Matteo

Hi @daviddemeij,

Though I am not sure if my proposed solution is the right solution for your issue, please let me elaborate the proposed workaround (I should have done that in the first place). I will be happy if this addresses your problem. Since majority of the code snippet given below uses a non-existent function, it is not a working code and therefore there could be minor errors. Otherwise please let me know.

Let us start with a code using a simple form:

import streamlit as st

def do_something(lower_threshold, upper_threshold):
    #take action here
    pass

lower_threshold = st.sidebar.text_input(label="Lower Threshold", value="0", key="na_lower")
upper_threshold = st.sidebar.text_input(label="Upper Threshold", value="100", key="na_upper")
st.sidebar.button(label="Submit", key="ta_submit")
do_something(lower_threshold, upper_threshold)

do_something() will be executed every time the value of an input widget is changed or a button is pressed – an undesirable situation which all of us are trying to avoid.

Now let us assume that we had a st function called get_last_used_widget_key() which returns the key of the widget last modified/pressed, perhaps an empty string in the first run. Let us now rewrite the relevant part of the code using the proposed function:

used_widget_key = st.get_last_used_widget_key()

lower_threshold = st.sidebar.text_input(label="Lower Threshold", value="0", key="na_lower")
upper_threshold = st.sidebar.text_input(label="Upper Threshold", value="100", key="na_upper")
st.sidebar.button(label="Submit", key="ta_submit")

if used_widget_key == "ta_submit":
    do_something(lower_threshold, upper_threshold)

Now do_something() is executed only if the submit button was pressed.

We can extend this for multiple forms and therefore multiple submit buttons in the app. I can either check for each of the submit buttons as given below:

used_widget_key = st.get_last_used_widget_key()

#Form 1
st.sidebar.subheader("Form 1")
lower_threshold_1 = st.sidebar.text_input(label="Lower Threshold", value="0", key="na_lower_1")
upper_threshold_1 = st.sidebar.text_input(label="Upper Threshold", value="100", key="na_upper_1")
st.sidebar.button(label="Submit", key="ta_submit_1")

#Form 2
st.sidebar.subheader("Form 2")
lower_threshold_2 = st.sidebar.text_input(label="Lower Threshold", value="100", key="na_lower_2")
upper_threshold_2 = st.sidebar.text_input(label="Upper Threshold", value="200", key="na_upper_2")
st.sidebar.button(label="Submit", key="ta_submit_2")

if used_widget_key == "ta_submit_1":
    do_something_1(lower_threshold_1, upper_threshold_1)
elif used_widget_key == "ta_submit_2":
    do_something_2(lower_threshold_2, upper_threshold_2)

Or, if there are too many forms and therefore submit buttons, I can also write the last part differently if a naming convention is used for the key argument – here a prefix “ta_” for buttons:

if used_widget_key[0:3] == "ta_":
    if used_widget_key == "ta_submit_1":
        do_something_1(lower_threshold_1, upper_threshold_1)
    elif used_widget_key == "ta_submit_2":
        do_something_2(lower_threshold_2, upper_threshold_2)

The difference between the last two code snippets appear to be somewhat superfluous but in a large multi-page application, a good code structure is always welcome.

Looking forward to suggestions for improvement or if an alternative solution already exists.

Hi @monchier,

Thanks for your prompt response and suggestion. I will open a feature request, taking into consideration additional feedback to this discussion thread.

I have outlined my proposed solution in the previous reply and, as mentioned in your reply, it assumes that re-run of the script is unavoidable when widget state changes. Risking a repetition, here is the suggestion - provide a get() API for the last widget used which triggered the re-run. Perhaps a name better than get_last_used_widget_key() is needed. This is just a placeholder.

Here is the key reason behind the suggested approach:

Unlike a few other approaches (eg. actual implementation of a form within streamlit) which would perhaps require changes in the internal design of streamlit (and therefore deferment or delay in implementation), this approach hopefully doesn’t require any change in design, just a get() function to expose the parameter already available internally.

I was thinking that an easier solution might be to add an extra optional argument to a widget called on_change which by default is set to on_change='update' meaning that the code will rerun when the value of the widget changes, which can be set to on_change='ignore' to ignore changes to the widget value and only rerun when another widget is changed (for example a button).

I haven’t delved deep into the code so not sure how viable this option is to implement in the current architecture, but from an API standpoint this seems like a logical way to do this.

3 Likes

Allow me to follow up on the discussion here. I think I got some of the comments along the thread, but I’m afraid I still don’t understand what is the usage of the key field for me as a user. I understand that you cannot have two widgets of the same type with the same key. But what is the usage of the key for the user? Maybe I’m using it wrong, but it feels like it forces me to duplicate code… For example:

I would like to have something like:

customer1 = st.selectbox("Select the customer: ", customers1)
customer2 = st.selectbox("Select the customer: ", customers2)

but I’m forced to have:

customer1 = st.selectbox("Select the customer: ", customers1, key="customer1")
customer2 = st.selectbox("Select the customer: ", customers2, key="customer2")

This is a little confusing and feels not needed. In particular because I don’t use the key but the symbol that the widget is assigned.

N.B. As I see that @Marc liked this, I’d mention that what I really want is to have the same selector/widget in every “page” (where page is using the approach in his awesome showcase).

1 Like

Hi @drorata,

What are the issues you’re having with your first snippet? The sample below seems to work fine for me.

import streamlit as st

c1 = ['a', 'b']
c2 = ['c', 'd']

customer1 = st.selectbox("Select the customer: ", c1)
customer2 = st.selectbox("Select the customer: ", c2)

customer1
customer2

If however you wanted to do something like the following, you would encounter a DuplicateWidgetID error with the message There are multiple identical st.selectbox widgets with the same generated key.

import streamlit as st

c1 = ['a', 'b']

customer1 = st.selectbox("Select the customer: ", c1)
customer2 = st.selectbox("Select the customer: ", c1)

customer1
customer2

The solution to this would be to use a key

import streamlit as st

c1 = ['a', 'b']

customer1 = st.selectbox("Select the customer: ", c1, key="a")
customer2 = st.selectbox("Select the customer: ", c1, key="b")

customer1
customer2

When a widget is created, it’s assigned an internal key based on its structure.
Multiple widgets with an identical structure will result in the same internal key, which causes this error.
To fix this, please pass a unique key argument to st.selectbox.

Thanks for the details. I faced exactly the second case indeed. I’m sorry for misleading in my OP. In that case, I understand why I have to use the key argument, but I argue it is likely to lead to something like:

customer1 = st.selectbox("Select the customer: ", customer_list, key="customer1")
customer2 = st.selectbox("Select the customer: ", customer_list, key="customer2")

This can happen when you want to choose two different customers from the same list. In this case, one has to use the key argument, but it is actually not really used by the users. If that’s indeed the case, I think streamlit should assign a key by itself. If the key can be used by the developer, then I don’t understand how.

3 Likes