Strange behavior with selectbox indexing custom objects

Summary

I get a ValueError saying that my custom object is not found in the iterable that I pass into st.selectbox; however, the UI renders as expected, is without issues, and is functional. There is some strange behavior happening in this indexing thing and I was hoping someone could tell me what’s going on behind the indexing and why this problem disappears if I add a key= argument in st.selectbox.

Steps to reproduce

Code snippet:

from dataclasses import dataclass

import streamlit as st


@dataclass
class Foo:
    name: str
    value: float


# Assume no duplicates.
things = (Foo('a', 0.2), Foo('b', 0.3), Foo('c', 0.1))

thing = st.selectbox('Select one', things)
st.header(f'Selected: {thing}')
  1. Run streamlit
streamlit strangebox.py
  1. Open browser.
  2. Change from option ‘a’ to whatever else, even back to ‘a’ itself.

Expected behavior:

The select option changes to the selected option without any error messages in the backend.

Actual behavior:

An error message appears:

Exception in thread ScriptRunner.scriptThread:
Traceback (most recent call last):
  File "/Users/muhammad.azman/.pyenv/versions/3.10.4/lib/python3.10/threading.py", line 1009, in _bootstrap_inner
    self.run()
  File "/Users/muhammad.azman/.pyenv/versions/3.10.4/lib/python3.10/threading.py", line 946, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 311, in _run_script_thread
    widget_states = self._session_state.get_widget_states()
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/runtime/state/safe_session_state.py", line 83, in get_widget_states
    return self._state.get_widget_states()
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/runtime/state/session_state.py", line 550, in get_widget_states
    return self._new_widget_state.as_widget_states()
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/runtime/state/session_state.py", line 223, in as_widget_states
    states = [
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/runtime/state/session_state.py", line 226, in <listcomp>
    if self.get_serialized(widget_id)
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/runtime/state/session_state.py", line 208, in get_serialized
    serialized = metadata.serializer(item.value)
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/elements/selectbox.py", line 58, in serialize
    return index_(self.options, v)
  File "/Users/muhammad.azman/Projects/spo-ingestors/.venv/lib/python3.10/site-packages/streamlit/util.py", line 142, in index_
    raise ValueError("{} is not in iterable".format(str(x)))
ValueError: Foo(name='c', value=0.1) is not in iterable

Debug info

  • Streamlit version: Streamlit, version 1.19.0
  • Python version: Python 3.10.4
  • Using Conda? PipEnv? PyEnv? Pex? pyenv 2.3.1
  • OS version: macOS Monterey v12.6
  • Browser version: Google Chrome v110.0.5481.177 (Official Build) (x86_64)

Requirements file

Using Conda? PipEnv? PyEnv? Pex?
pyenv 2.3.1, under virtual environment

requirements:

streamlit

Links

N/A

Additional information

Did a little digging and placing print statements. Here’s what I found:

Events as they occured:

  1. Open browser:
in streamlit.runtime.state.session_state.get_serialized()
field='int_value'
item.value=Foo(name='a', value=0.2)

in streamlist.util.index_()
i=0 value=Foo(name='a', value=0.2)(4862353488) x=Foo(name='a', value=0.2)(4862358528): Match!

in streamlit.runtime.state.session_state.get_serialized()
field='int_value'
item.value=Foo(name='a', value=0.2)

in streamlist.util.index_()
i=0 value=Foo(name='a', value=0.2)(4862353488) x=Foo(name='a', value=0.2)(4862358528): Match!
  1. Change to option ‘b’
in streamlit.runtime.state.session_state.get_serialized()
field='int_value'
item.value=Foo(name='b', value=0.3)

in streamlist.util.index_()
i=0 value=Foo(name='a', value=0.2)(4862356848) x=Foo(name='b', value=0.3)(4862353680): Different.
i=1 value=Foo(name='b', value=0.3)(4862356704) x=Foo(name='b', value=0.3)(4862353680): Different.
i=2 value=Foo(name='c', value=0.1)(4914496944) x=Foo(name='b', value=0.3)(4862353680): Different.
DOH!
Exception in thread ScriptRunner.scriptThread:
... (see stack trace above)

Number in the parens in the logs are the object id you get using the id() python built-in. No idea why it works like this.

Implementing an __eq__ dunder for my class did the trick but there’s nothing in the docs that would hint to if I would need to implement it for custom objects.

2 Likes

This is because of how Streamlit reruns from the top each time. You are creating new Foo objects with each interaction. So you need to tell Python how to tell if two Foos are the same, or you need to stash them in session_state so they aren’t recreated with each page run.

Using session_state to prevent new objects being created:

from dataclasses import dataclass
import streamlit as st

@dataclass
class Foo:
    name: str
    value: float

# Assume no duplicates.
if 'things' not in st.session_state:
    st.session_state.things = (Foo('a', 0.2), Foo('b', 0.3), Foo('c', 0.1))

things = st.session_state.things

thing = st.selectbox('Select one', things)
st.header(f'Selected: {thing}')

Defining dunder method to specify equivalence:

from __future__ import annotations
from dataclasses import dataclass
import streamlit as st

@dataclass
class Foo:
    name: str
    value: float

    def __eq__(self, other:Foo):
        if self.name == other.name and self.value == other.value:
            return True
        return False

# Assume no duplicates.
things = (Foo('a', 0.2), Foo('b', 0.3), Foo('c', 0.1))

thing = st.selectbox('Select one', things)
st.header(f'Selected: {thing}')
2 Likes

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.