Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

st.selectbox could be typed so that it always returns T unless the index is None (etc) #8717

Open
2 tasks done
wyattscarpenter opened this issue May 20, 2024 · 4 comments
Open
2 tasks done
Labels
feature:st.selectbox type:enhancement Requests for feature enhancements or new features

Comments

@wyattscarpenter
Copy link

Checklist

  • I have searched the existing issues for similar feature requests.
  • I added a descriptive title and summary to this issue.

Summary

Currently, the return type of selectbox is T | None. As far as I know, the only way for selectbox to return None is if index is None or if OptionSequence[T] is empty (OptionSequence[]?). These conditions could be added to the type signature of selectbox using overloads, and then in my program I wouldn't have to assert or str the results of every st.selectbox. There would be one or two overloads that returned None | T and one that returned just T. NoReturn could also possibly be added to the return type union, because if you choose an invalid index the function will error and never return.

Or maybe this goal is doomed. My project uses a lot of https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state#session-state-and-widget-state-association stuff like:

import streamlit as st
st.session_state.name = "Max"
st.selectbox("Your name", ["Sam", "Max"], key="name")

and I don't really know how to account for this in static typing, because the program could set the session_state value of the widget to None.

Furthermore, it may be unavoidable that there's always the possibility of an error in rendering, or suchlike, that would cause the widget to return None. I don't know enough to say.

So, I think this typing feature is desirable, but it may be impossible.

Why?

Currently, whenever I use a selectbox in my typed project, I have to deal with the type union somehow each time, even though the result should always be of a certain type, which is a bit annoying.

How?

No response

Additional Context

No response

@wyattscarpenter wyattscarpenter added the type:enhancement Requests for feature enhancements or new features label May 20, 2024
Copy link

To help Streamlit prioritize this feature, react with a 👍 (thumbs up emoji) to the initial post.

Your vote helps us identify which enhancements matter most to our users.

Visits

@wyattscarpenter wyattscarpenter changed the title st.selectbox could be typed so that it always returns T unless the index is None st.selectbox could be typed so that it always returns T unless the index is None (etc) May 20, 2024
@wyattscarpenter
Copy link
Author

It seems like

st.session_state.name = "Max"
st.selectbox("Your name", ["Sam", "Max"], key="name")

isn't even documented on the session state page, only

st.text_input("Your name", key="name")

# This exists now:
st.session_state.name

Perhaps it would be worth it (to me) to give up pre-widget key-setting, if that could lead to facilitating better static typing guarantees. In that case it would be nice for selectbox to have an value parameter so I don't have to write in tons of value→index mapping logic into the index parameters I would have to be using. After reading #1532 I'm fairly certain I could get the result I want using value parameters.

@Asaurus1
Copy link
Contributor

Asaurus1 commented May 21, 2024

I would add onto this that it seems like the latest versions of st.number_input is typed to always return a Number regardless of the input argument types.. this makes it more annoying to write

integer: int = st.number_input("Number of things:", min_value=0, max_value=10)

because typecheckers complain that number_input could return a float. The widget enforces type consistency internally, and the typing should reflect that.

@wyattscarpenter
Copy link
Author

Here's my workaround, for the time being:

def typesafe_selectbox(label: str, options: Sequence[T], default: T|None = None, **kwargs: Any) -> T:
  """Call `st.selectbox` but don't pollute your type with `None` in the process;
  if the selectbox would return `None`, return the value passed in as `default`.
  If `default` is `None` (eg: not passed in), the value of `options[0]` is used.
  The value of `default` (unless `None`) must be in `options`, on pain of runtime error;
  and, furthermore, `options` must not be empty.

  `options` is also a `Sequence` type rather than the broader `Iterable`
  to ensure it isn't an exhaustible iterator that would be harmed by a call to `.index()`

  `default` is named in analogy to a parameter in `st.multiselect`.
  But there are other widgets, like `st.text_input`, that have an analogous parameter named `value` 🤷.

  All arguments, including kwargs, are passed on to st.selectbox, either directly or indirectly.
  It's not clear to me if there's a better & concise way to do the type signature of kwargs here.
  
  Note that if you use st.session_state to set the value of the key of the selectbox, that takes priority over the `default` argument.
  However, if you set the value of said key to `None`, this function will still return `options[0]`."""
  i = 0 if default is None else options.index(default)
  x = st.selectbox(label, options, index=i, **kwargs)
  return x if x is not None else options[i]

Here's some example usage code:

st.write(typesafe_selectbox("test box empty str", ['', 'foo']))
st.write(typesafe_selectbox("test box empty str", ['']))
#st.write(typesafe_selectbox("test box empty list", [])) #this is an error
st.write(typesafe_selectbox("test box", ['a']))
st.write(typesafe_selectbox("test box2", ['a'], default='a'))
st.write(typesafe_selectbox("test box3", ['a', 'b'], default='b'))
st.write(typesafe_selectbox("test box4", ['a', 'b'], default='a'))
#unfortunately, this still displays as empty, even though it returns the first value:
st.session_state.tampered_key = None
st.write(typesafe_selectbox("test box5", ['a', 'b'], key="tampered_key"))
# This displays as, and returns, 'b'
st.session_state.tampered_key2 = 'b'
st.write(typesafe_selectbox("test box6", ['a', 'b'], key="tampered_key2", default='a'))
# st.session_state.tampered_key3 = 1 #this would be an error
# st.write(typesafe_selectbox("test box5", ['a', 'b'], key="tampered_key3"))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature:st.selectbox type:enhancement Requests for feature enhancements or new features
Projects
None yet
Development

No branches or pull requests

3 participants