Checklist
Summary
Problem
When building complex, dynamic forms in Streamlit (e.g., configuration builders, dynamic rows with reordering, or nested JSON editors), st.session_state forces developers into a flat key-value architecture.
Why?
Currently, if a user wants to update a nested dictionary or list, they have to use flat keys like key=f"rep_sales_{row_id}_email" and then write boilerplate code (either on_change callbacks or post-run loop reconstruction) to reassemble those flat keys back into a nested Python dictionary or data class. This leads to bloated code, higher risk of state desync (especially when reordering UI elements), and poor developer ergonomics.
How?
Proposed Solution
Allow the key parameter in input widgets to accept multi-level paths, automatically updating the deeply nested structure inside st.session_state when the widget value changes. Here are three potential API designs:
Option A: State Reference Proxy (Best for IDE Type Hints & Autocomplete)
Introduce a st.path proxy object or a st.bind method. This allows developers to use standard dictionary/attribute access—enabling IDE type hints and auto-completion—without Python prematurely evaluating the value before Streamlit sees it.
# Approach 1: Path Proxy
st.text_input(
"Sales Rep Email",
key=st.path.representatives["sales"][2]["email"]
)
# Approach 2: Object Binding (excellent for typed dataclasses)
rep = st.session_state.representatives["sales"][2]
st.text_input(
"Sales Rep Email",
key=st.bind(rep, "email")
)
Option B: String-based Pathing
Allow a string that represents the path. While this lacks strict type-hinting, it is highly readable and easy to implement using standard dictionary traversal.
# Automatically updates st.session_state.representatives["sales"][2]["email"]
st.text_input(
"Sales Rep Email",
key='representatives["sales"][2]["email"]',
multi_level_key=True
)
Option C: Tuple/List Pathing (Pythonic Backend Approach)
This avoids string parsing entirely, providing a clean programmatic way to define the traversal path.
# Automatically updates st.session_state.representatives["sales"][2]["email"]
st.text_input(
"Sales Rep Email",
key=("representatives", "sales", 2, "email")
)
Expected Behavior
- If the base key (
representatives) does not exist in st.session_state, Streamlit should throw a KeyError (or initialize it, depending on preferred design).
- If the path exists, when the user types in the widget, Streamlit natively traverses the nested dictionary/list and updates the final leaf node with the new widget value.
- The widget reads its initial
value directly from that nested path, eliminating the need to manually pass value=st.session_state["representatives"]["sales"][2]["email"].
Alternatives Considered
on_change callbacks with kwargs: Developers currently do this by passing the path arguments to a callback function that mutates the dictionary. This requires declaring a separate function for every type of input, which gets incredibly tedious.
- Third-party state wrappers: Using external libraries or custom Python
@property wrappers to handle the sync between flat widget keys and nested object structures. While functional, nested structures are common enough that native support would vastly improve the developer experience.
Additional Context
As Streamlit apps grow from simple scripts into full-fledged CRUD applications and internal tools, dynamic layouts (like adding/removing rows of structured data) become the norm. Tying widget state directly to a durable, deeply nested structure rather than a flat string would solve an entire class of state-sync bugs related to dynamic UIs and list reordering.
Checklist
Summary
Problem
When building complex, dynamic forms in Streamlit (e.g., configuration builders, dynamic rows with reordering, or nested JSON editors),
st.session_stateforces developers into a flat key-value architecture.Why?
Currently, if a user wants to update a nested dictionary or list, they have to use flat keys like
key=f"rep_sales_{row_id}_email"and then write boilerplate code (eitheron_changecallbacks or post-run loop reconstruction) to reassemble those flat keys back into a nested Python dictionary or data class. This leads to bloated code, higher risk of state desync (especially when reordering UI elements), and poor developer ergonomics.How?
Proposed Solution
Allow the
keyparameter in input widgets to accept multi-level paths, automatically updating the deeply nested structure insidest.session_statewhen the widget value changes. Here are three potential API designs:Option A: State Reference Proxy (Best for IDE Type Hints & Autocomplete)
Introduce a
st.pathproxy object or ast.bindmethod. This allows developers to use standard dictionary/attribute access—enabling IDE type hints and auto-completion—without Python prematurely evaluating the value before Streamlit sees it.Option B: String-based Pathing
Allow a string that represents the path. While this lacks strict type-hinting, it is highly readable and easy to implement using standard dictionary traversal.
Option C: Tuple/List Pathing (Pythonic Backend Approach)
This avoids string parsing entirely, providing a clean programmatic way to define the traversal path.
Expected Behavior
representatives) does not exist inst.session_state, Streamlit should throw aKeyError(or initialize it, depending on preferred design).valuedirectly from that nested path, eliminating the need to manually passvalue=st.session_state["representatives"]["sales"][2]["email"].Alternatives Considered
on_changecallbacks withkwargs: Developers currently do this by passing the path arguments to a callback function that mutates the dictionary. This requires declaring a separate function for every type of input, which gets incredibly tedious.@propertywrappers to handle the sync between flat widget keys and nested object structures. While functional, nested structures are common enough that native support would vastly improve the developer experience.Additional Context
As Streamlit apps grow from simple scripts into full-fledged CRUD applications and internal tools, dynamic layouts (like adding/removing rows of structured data) become the norm. Tying widget state directly to a durable, deeply nested structure rather than a flat string would solve an entire class of state-sync bugs related to dynamic UIs and list reordering.