Proposal for a more useful key parameter (feat. Session State, REST API and HTTP query)

Hello everyone :wave:

So far with Streamlit, we can assign custom user keys to widgets. It can be used to avoid duplicate errors when two widgets generate the same hash. In my experience, I haven’t encountered any other use for this parameter. However, I think it could be very powerful by its nature: bind a name to a widget.

TL;DR: The core idea is to bind Streamlit widgets to session state variables by using the key parameter, and allow direct access to those state variables from HTTP queries or from an auto-generated REST or GraphQL API.

Session state

The key parameter would be used to bind a widget to a state variable.

For this part, I’ll take as an example my implementation of a settings page with persisting state values.

What I don’t like in my code is this part:

state.input = st.text_input("Set input value.", state.input or "")
state.slider = st.slider("Set slider value.", 1, 10, state.slider)
state.radio = st.radio("Set radio value.", options, options.index(state.radio) if state.radio else 0)
state.checkbox = st.checkbox("Set checkbox value.", state.checkbox)
state.selectbox = st.selectbox("Select value.", options, options.index(state.selectbox) if state.selectbox else 0)
state.multiselect = st.multiselect("Select value(s).", options, state.multiselect)

It’s way to complex for what I want to do. However, using the key parameter, we could instead write something like this:

import streamlit as st
import streamlit.state as state

options = ["Hello", "World", "Goodbye"]

state(my_var=1, another_var=2)  # Create new state variables with default values
state(yet_another_var=5)        # Can be called anytime in the code to create new state variables

state.my_var = 3  # Also assign a state value

st.text_input("Set input value.", key="my_input")
st.slider("Set slider value.", 1, 10, key="my_slider")
local_var = st.radio("Set radio value.", options, key="my_radio")

st.write(state.my_var, state.my_input, local_var)

In this example:

  • You can assign values directly to your state, like my_var.
  • The user would not need to set state variables manually, it’d be handled internally thanks to the key argument.
  • In case of undefined state variables, safe defaults like options.index(state.radio) if state.radio else 0 wouldn’t be needed anymore.
  • The user would still be able to define local variables from widget return values, of course.

HTTP query strings

Streamlit could retrieve parameters from query strings, and automatically assign values to your state. Widgets bound to a state variable thanks to their key would be automatically updated.

The initial idea of query strings is inspired from this pull request.

import streamlit as st
import streamlit.state as state

options = ["Hello", "World", "Goodbye"]

state(my_var=3, another_var=2)  # Set default state values, can be overridden by an HTTP query
state(yet_another_var=5)        # Can be called anytime in the code to create new state variables

#state.my_var = 3  # Assign a state value that overrides HTTP query

st.text_input("Set input value.", key="my_input")
st.slider("Set slider value.", 1, 10, key="my_slider")
local_var = st.radio("Set radio value.", options, key="my_radio")

st.write(state.my_var, state.my_input, local_var)

An example of query string:

http://localhost:8501/?my_var=5&my_slider=4&my_radio=World

This way, you wouldn’t need to use a st.get_url() to manually retrieve HTTP parameters, as those would be automatically assigned to your state. However, if one updates the values in the front-end, you would need a way to update automatically the query string, and more importantly, choose the state variables you want to update. We could do something like this to register state variables for update:

st.update_url("my_var", "my_radio")

Here I indicate which state variables will be updated in the query string. Unlike the aforementioned pull request, I don’t need to use any st.set_url() function. Which means, if I update a state value, or a widget associated with a key, the query string would automatically change like so:

http://localhost:8501/?my_var=3&my_radio=Goodbye

If my_slider is not manually set in the query string by the user, it won’t appear at all if not specified in update_url(). IMO such function is needed when dealing with complex state values like DataFrames that can be ignored from the query string.

REST API

Like HTTP query strings, the REST API would directly set and retrieve values from state variables. Likewise, widgets bound to a state variable with their key would be automatically updated.

I saw this great idea from this discussion: Streamlit restful app. From there, @thiago opened a GitHub issue and mentioned the idea of auto-generating a REST API. It would be so great to avoid that need of implementing boilerplate code to define routes, get and set values, and so on. Maybe for a finer control over your API, some function could be implemented later on, but I think providing a simple REST API remains possible with no additional function.

import streamlit as st
import streamlit.state as state

options = ["Hello", "World", "Goodbye"]

state(my_var=3, another_var=2)  # Set default state values, can be overridden by an HTTP query or a REST value
state(yet_another_var=5)        # Can be called anytime in the code to create new state variables

#state.my_var = 3  # Assign a state value that overrides HTTP query or REST value

st.text_input("Set input value.", key="my_input")
st.slider("Set slider value.", 1, 10, key="my_slider")
local_var = st.radio("Set radio value.", options, key="my_radio")

st.write(state.my_var, state.my_input, local_var)

And REST queries:

GET /rest/my_var
{ "my_var": 3 }

GET /rest/my_var/5
{ "my_var": 5 }

GET /rest/my_var/5?my_slider=4&my_radio=World
GET /rest/my_var?my_var=5&my_slider=4&my_radio=World
{ "my_var": 5 }

GET /rest/my_radio?my_var=5&my_slider=4&my_radio=World
{ "my_radio": "World" }

Here, the REST API would directly interact with state variables. And maybe with a persisting session we could do something like successive POSTs/PUTs instead of passing parameters when doing a GET. Just a quick supposition.

Another (better?) approach would be to return the whole session state by default, or only registered state variables like suggested with HTTP queries.

With the python snipped above, we’d have this result:

GET /rest/?my_var=5&my_slider=4&my_radio=World
{
  "state": {
    "another_var": 2,
    "my_radio": "World",
    "my_slider": 4,
    "my_var": 5,
    "yet_another_var": 5
  }
}

And with registered state variables:

st.register_rest("my_radio", "my_var")

We’d have:

GET /rest/?my_var=5&my_slider=4&my_radio=World
{
  "state": {
    "my_radio": "World",
    "my_var": 5
  }
}

GraphQL API (Bonus)

If implementing a REST API like above is possible, we could imagine communicating with GraphQL queries as well!

This query…

{
  state {
    my_radio,
    my_var
  }
}

… would return:

{
  "state": {
    "my_radio": "World",
    "my_var": 5
  }
}

That’s all folks!

I hope this topic will bring some new ideas on how to interact with Streamlit apps.

They are possibly some cases not covered with such mechanism I haven’t thought of, or maybe the way streamlit was built does not provide a simple way to implement something like this internally. I’ll update this post in case it needs some clarification.

However, in my opinion, if we could allow people to interact with their app like above, just by naming widgets and nothing more, it could be a killer feature against other alternative frameworks, and even jupyter notebooks!

3 Likes

I’m not really qualified to evaluate all the merits here, but thanks for starting the discussion!

I wonder if overloading key in this way might make things excessively complex? Meaning, once the key stops being used just for de-duplication and to control various other advanced behaviors, people will need to start thinking about a whole lot more. Like I said, don’t know, that was just my first reaction.

1 Like

Thanks for your reply @randyzwitch.

You’re right, using one parameter for multiple things can make things more complex to grasp and appear bloated. In this case, maybe creating a dedicated state or name parameter would be more appropriate.

Just to not lose this when the time comes to talk about programmable state, let me link it back to the Programmable State conversation :wink: