Repository: streamlit/streamlit

Issue:
Discrepancy between value shown and value returned by widged when it is redisplayed
### Summary

When a widget is redisplayed, the value returned is not the same value the widget shows.

### Steps to reproduce

Here an example with multiselect:

```
import streamlit

letter = streamlit.sidebar.radio("Letter", ["A", "B"])
if letter == "A":
    numbers = streamlit.multiselect("Number", ["1", "2"])
else:
    numbers = streamlit.multiselect("Number", ["3", "4"])

if len(numbers) == 0:
    streamlit.write("I didn't compute anything because no number is selected")
else:    
    streamlit.write(f"I started an expensive computation using the chosen letter {letter} and the currently selected numbers {numbers}")
```

1. With 'A' selected, select the numbers 1 and 2
2. Change the sidebar selection to 'B', then again to 'A'

**Expected behavior:**

Either 
1) the multiselect widget is empty and shows "Select an option"; the message seen is "I didn't compute anything because no number is selected" (which I prefer)
Or
2) the multiselect widget has the last values selected when it was shown the last time (1 and 2) and the message seen is "I started an expensive computation using the chosen letter A and the currently selected numbers [1, 2]

**Actual behavior:**

The multiselect widget is empty and shows 'Select an option' , but the message seen is "I started an expensive computation using the chosen letter A and the currently selected numbers [1, 2]"

**Workaround**

To get the 2nd expected behavior, add the arguments key="key", default = streamlit.session_state["key"]

To get the 1st behavior, 🤷‍♂️

### Debug info

- Streamlit version: 0.84.0
- Python version: 3.9.5
- Using Conda

### Is this a regression?
With streamlit 0.82.0, I got the behavior (1)


Discussion:
Hey @clo-vis Thank you for the report! This is definitely a weird reaction, and I think our session_state preserves the value, which is rather weird. We've prioritized this in.

Resolving patch:
diff --git a/e2e/scripts/redisplayed_widgets.py b/e2e/scripts/redisplayed_widgets.py
new file mode 100644
--- /dev/null
+++ b/e2e/scripts/redisplayed_widgets.py
@@ -0,0 +1,24 @@
+# Copyright 2018-2021 Streamlit Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import streamlit as st
+
+if st._is_running_with_streamlit:
+
+    if st.checkbox("checkbox 1"):
+        if st.checkbox("checkbox 2"):
+            st.write("hello")
+
+        if st.checkbox("checkbox 3", key="c3"):
+            st.write("goodbye")
diff --git a/lib/streamlit/state/session_state.py b/lib/streamlit/state/session_state.py
--- a/lib/streamlit/state/session_state.py
+++ b/lib/streamlit/state/session_state.py
@@ -412,26 +412,51 @@ def reset_triggers(self) -> None:
     def cull_nonexistent(self, widget_ids: Set[str]):
         self._new_widget_state.cull_nonexistent(widget_ids)
 
+        # Remove entries from _old_state corresponding to widgets not in
+        # widget_ids that do *not* have a user-defined key.
+        self._old_state = {
+            k: v
+            for k, v in self._old_state.items()
+            if (k in widget_ids or not k.startswith(GENERATED_WIDGET_KEY_PREFIX))
+        }
+
     def set_metadata(self, widget_metadata: WidgetMetadata) -> None:
         widget_id = widget_metadata.id
         self._new_widget_state.widget_metadata[widget_id] = widget_metadata
 
     def maybe_set_state_value(self, widget_id: str) -> bool:
+        """Keep widget_state and session_state in sync when a widget is registered.
+
+        This method returns whether the frontend needs to be updated with the
+        new value of this widget.
+        """
         widget_metadata = self._new_widget_state.widget_metadata[widget_id]
         deserializer = widget_metadata.deserializer
         initial_value = deserializer(None, widget_metadata.id)
 
         if widget_id not in self:
+            # This is the first time this widget is being registered, so we set
+            # its value in session_state and remember its initial_value.
             self._old_state[widget_id] = initial_value
             self._initial_widget_values[widget_id] = initial_value
+
         elif (
             widget_id in self._initial_widget_values
             and initial_value != self._initial_widget_values[widget_id]
         ):
+            # The initial_value of this widget has been changed (most likely by
+            # the user live-editing their script), so we update its value in
+            # widget_state and remember the new initial_value.
             self._new_widget_state.set_from_value(widget_id, initial_value)
             self._initial_widget_values[widget_id] = initial_value
             return True
 
+        elif widget_id in self and widget_id not in self._new_widget_state:
+            # This widget is being registered, but it had its value initially
+            # set via st.session_state, so we set it in widget_state to match.
+            self._new_widget_state.set_from_value(widget_id, self[widget_id])
+            return True
+
         return False
 
     def get_value_for_registration(self, widget_id: str) -> Any:


Test patch:
diff --git a/lib/tests/streamlit/state/session_state_test.py b/lib/tests/streamlit/state/session_state_test.py
--- a/lib/tests/streamlit/state/session_state_test.py
+++ b/lib/tests/streamlit/state/session_state_test.py
@@ -504,10 +504,30 @@ def test_widget_changed(self):
         self.session_state._new_widget_state["foo"] = "bar"
         assert not self.session_state._widget_changed("foo")
 
+    def test_cull_nonexistent(self):
+        generated_widget_key = f"{GENERATED_WIDGET_KEY_PREFIX}-removed_widget"
+
+        self.session_state._old_state = {
+            "existing_widget": True,
+            generated_widget_key: True,
+            "val_set_via_state": 5,
+        }
+
+        wstates = WStates()
+        self.session_state._new_widget_state = wstates
+
+        self.session_state.cull_nonexistent({"existing_widget"})
+
+        assert self.session_state["existing_widget"] == True
+        assert generated_widget_key not in self.session_state
+        assert self.session_state["val_set_via_state"] == 5
+
     def test_maybe_set_state_value(self):
         wstates = WStates()
         self.session_state._new_widget_state = wstates
 
+        # The widget is being registered for the first time, so there's no need
+        # to have the frontend update with a new value.
         wstates.set_widget_metadata(
             WidgetMetadata(
                 id="widget_id_1",
@@ -519,6 +539,8 @@ def test_maybe_set_state_value(self):
         assert self.session_state.maybe_set_state_value("widget_id_1") == False
         assert self.session_state["widget_id_1"] == 0
 
+        # The initial value of this widget has changed, so we need to update
+        # it on the client.
         wstates.set_widget_metadata(
             WidgetMetadata(
                 id="widget_id_1",
@@ -530,6 +552,14 @@ def test_maybe_set_state_value(self):
         assert self.session_state.maybe_set_state_value("widget_id_1") == True
         assert self.session_state["widget_id_1"] == 1
 
+        # This widget's value was set via st.session_state before the widget was
+        # registered, so we need to update it on the client.
+        del self.session_state._initial_widget_values["widget_id_1"]
+        del self.session_state._new_widget_state["widget_id_1"]
+        self.session_state._old_state["widget_id_1"] = 2
+        assert self.session_state.maybe_set_state_value("widget_id_1") == True
+        assert self.session_state["widget_id_1"] == 2
+
 
 @patch(
     "streamlit.state.session_state.get_session_state",