Skip to content

Allow dynamic changes to st.select_slider options when key is provided#13696

Merged
lukasmasuch merged 17 commits intodevelopfrom
lukasmasuch/select-slider-dynamic
Jan 27, 2026
Merged

Allow dynamic changes to st.select_slider options when key is provided#13696
lukasmasuch merged 17 commits intodevelopfrom
lukasmasuch/select-slider-dynamic

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Jan 26, 2026

Describe your changes

Enable st.select_slider to support dynamic option changes when a key is provided. Converts the widget from index-based to string-based session state storage, following the same pattern used by st.radio, st.selectbox, and st.multiselect. This allows the widget to maintain its value when options change, as long as the selected option still exists in the new options list.

Changes:

  • Add raw_value field to Slider.proto for string-based value storage
  • Refactor SelectSliderSerde to use formatted option strings instead of indices
  • Change key_as_main_identity to True for stable widget identity with dynamic updates
  • Use string_array_value instead of double_array_value for session state
  • Add value validation function to handle option changes gracefully
  • Update frontend to convert between string values and indices
  • Add comprehensive unit and E2E tests

GitHub Issue Link (if applicable)

Testing Plan

  • Added unit and e2e tests.

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

…dentity

Convert select_slider from index-based to string-based session state storage,
enabling dynamic option updates while preserving valid selections. Aligns with
patterns used by st.radio, st.selectbox, and st.multiselect.

Co-Authored-By: Claude <[email protected]>
@lukasmasuch lukasmasuch requested a review from a team as a code owner January 26, 2026 19:04
Copilot AI review requested due to automatic review settings January 26, 2026 19:04
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Jan 26, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 26, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13696/streamlit-1.53.1-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13696.streamlit.app (☁️ Deploy here if not accessible)

@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users security-assessment-completed labels Jan 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request enables st.select_slider to support dynamic option changes when a key is provided, aligning its behavior with st.radio, st.selectbox, and st.multiselect. The implementation converts the widget from index-based to string-based session state storage using formatted option strings, allowing the widget to maintain its value when options change (as long as the selected option still exists in the new options list).

Changes:

  • Converts select_slider from index-based to string-based value storage for dynamic option support
  • Changes key_as_main_identity from selective parameters to True for stable widget identity
  • Adds comprehensive validation logic to handle option changes gracefully (reset invalid values to defaults)

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
proto/streamlit/proto/Slider.proto Adds raw_value field for string-based value storage in select_slider
lib/streamlit/elements/widgets/select_slider.py Refactors SelectSliderSerde to use formatted strings instead of indices; adds value validation
lib/streamlit/elements/lib/options_selector_utils.py Adds validate_and_sync_select_slider_value_with_options for validating values against current options
frontend/lib/src/components/widgets/Slider/Slider.tsx Adds string/index conversion logic and rawValue proto handling for select_slider
frontend/lib/src/components/widgets/Slider/Slider.test.tsx Adds TypeScript tests for string array value serialization and rawValue handling
lib/tests/streamlit/elements/select_slider_test.py Adds Python unit tests for dynamic options, range sliders, and format_func; updates ID stability tests
e2e_playwright/st_select_slider.py Adds test app code for dynamic options scenarios
e2e_playwright/st_select_slider_test.py Adds E2E tests for dynamic option preservation and reset behavior

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

- Fix mypy errors in select_slider.py using cast instead of assertions
- Update element_tree.py SelectSliderSerde usage for new signature
- Improve serialize/deserialize to detect range values from data
- Update validation function to handle ranges set via session state
- Update enum coercion tests to reflect string-based serialization behavior

Co-Authored-By: Claude <[email protected]>
lukasmasuch and others added 3 commits January 26, 2026 20:42
- Fix serialize() and validation to accept both tuple and list for range
  values, matching the API signature which allows both types
- Add negative assertions to E2E, Python, and TypeScript tests to catch
  regressions
- Add test coverage for dynamic options with Enum values

Co-Authored-By: Claude <[email protected]>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 26, 2026

📉 Frontend coverage change detected

The frontend unit test (vitest) coverage has decreased by 0.0300%

  • Current PR: 86.2500% (13540 lines, 1861 missed)
  • Latest develop: 86.2800% (13515 lines, 1854 missed)

💡 Consider adding more unit tests to maintain or improve coverage.

📊 View detailed coverage comparison

@lukasmasuch lukasmasuch changed the title Enable dynamic options for st.select_slider Allow dynamic changes to st.select_slider options when key is provided Jan 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 26, 2026

📉 Python coverage change detected

The Python unit test coverage has decreased by 0.0100%

  • Current PR: 93.1166% (23157 statements, 1594 missed)
  • Latest develop: 93.1266% (23118 statements, 1589 missed)

✅ Coverage change is within normal range.

Coverage by files
Name Stmts Miss Cover
streamlit/__init__.py 136 0 100%
streamlit/__main__.py 3 3 0%
streamlit/auth_util.py 231 25 89%
streamlit/cli_util.py 39 6 85%
streamlit/column_config.py 3 0 100%
streamlit/commands/__init__.py 0 0 100%
streamlit/commands/echo.py 54 2 96%
streamlit/commands/execution_control.py 70 10 86%
streamlit/commands/logo.py 53 1 98%
streamlit/commands/navigation.py 106 2 98%
streamlit/commands/page_config.py 106 4 96%
streamlit/components/__init__.py 0 0 100%
streamlit/components/lib/__init__.py 0 0 100%
streamlit/components/lib/local_component_registry.py 35 2 94%
streamlit/components/types/__init__.py 0 0 100%
streamlit/components/types/base_component_registry.py 14 0 100%
streamlit/components/types/base_custom_component.py 48 6 88%
streamlit/components/v1/__init__.py 5 0 100%
streamlit/components/v1/component_arrow.py 33 2 94%
streamlit/components/v1/component_registry.py 41 3 93%
streamlit/components/v1/components.py 4 4 0%
streamlit/components/v1/custom_component.py 84 7 92%
streamlit/components/v2/__init__.py 27 0 100%
streamlit/components/v2/bidi_component/__init__.py 4 0 100%
streamlit/components/v2/bidi_component/constants.py 5 0 100%
streamlit/components/v2/bidi_component/main.py 148 17 89%
streamlit/components/v2/bidi_component/serialization.py 81 2 98%
streamlit/components/v2/bidi_component/state.py 13 0 100%
streamlit/components/v2/component_definition_resolver.py 30 0 100%
streamlit/components/v2/component_file_watcher.py 117 9 92%
streamlit/components/v2/component_manager.py 97 13 87%
streamlit/components/v2/component_manifest_handler.py 24 0 100%
streamlit/components/v2/component_path_utils.py 68 5 93%
streamlit/components/v2/component_registry.py 121 8 93%
streamlit/components/v2/get_bidi_component_manager.py 8 1 88%
streamlit/components/v2/manifest_scanner.py 227 25 89%
streamlit/components/v2/presentation.py 84 19 77%
streamlit/components/v2/types.py 8 8 0%
streamlit/config.py 415 12 97%
streamlit/config_option.py 79 3 96%
streamlit/config_util.py 288 7 98%
streamlit/connections/__init__.py 6 0 100%
streamlit/connections/base_connection.py 49 0 100%
streamlit/connections/snowflake_connection.py 98 16 84%
streamlit/connections/snowpark_connection.py 44 3 93%
streamlit/connections/sql_connection.py 56 6 89%
streamlit/connections/util.py 33 0 100%
streamlit/cursor.py 130 1 99%
streamlit/dataframe_util.py 506 47 91%
streamlit/delta_generator.py 250 7 97%
streamlit/delta_generator_singletons.py 74 7 91%
streamlit/deprecation_util.py 66 4 94%
streamlit/development.py 1 0 100%
streamlit/elements/__init__.py 0 0 100%
streamlit/elements/alert.py 60 0 100%
streamlit/elements/arrow.py 203 15 93%
streamlit/elements/balloons.py 10 0 100%
streamlit/elements/bokeh_chart.py 9 0 100%
streamlit/elements/code.py 20 1 95%
streamlit/elements/deck_gl_json_chart.py 104 10 90%
streamlit/elements/dialog_decorator.py 38 0 100%
streamlit/elements/doc_string.py 227 9 96%
streamlit/elements/empty.py 16 4 75%
streamlit/elements/exception.py 101 10 90%
streamlit/elements/form.py 56 2 96%
streamlit/elements/graphviz_chart.py 36 1 97%
streamlit/elements/heading.py 56 0 100%
streamlit/elements/html.py 49 0 100%
streamlit/elements/iframe.py 29 0 100%
streamlit/elements/image.py 32 0 100%
streamlit/elements/json.py 48 6 88%
streamlit/elements/layouts.py 140 3 98%
streamlit/elements/lib/__init__.py 0 0 100%
streamlit/elements/lib/built_in_chart_utils.py 391 26 93%
streamlit/elements/lib/color_util.py 100 4 96%
streamlit/elements/lib/column_config_utils.py 169 1 99%
streamlit/elements/lib/column_types.py 190 4 98%
streamlit/elements/lib/dialog.py 69 1 99%
streamlit/elements/lib/dicttools.py 39 2 95%
streamlit/elements/lib/file_uploader_utils.py 30 0 100%
streamlit/elements/lib/form_utils.py 26 0 100%
streamlit/elements/lib/image_utils.py 176 21 88%
streamlit/elements/lib/js_number.py 28 3 89%
streamlit/elements/lib/layout_utils.py 121 1 99%
streamlit/elements/lib/mutable_status_container.py 73 4 95%
streamlit/elements/lib/options_selector_utils.py 142 2 99%
streamlit/elements/lib/pandas_styler_utils.py 80 2 98%
streamlit/elements/lib/policies.py 56 1 98%
streamlit/elements/lib/shortcut_utils.py 42 2 95%
streamlit/elements/lib/streamlit_plotly_theme.py 48 0 100%
streamlit/elements/lib/subtitle_utils.py 76 5 93%
streamlit/elements/lib/utils.py 76 5 93%
streamlit/elements/map.py 110 1 99%
streamlit/elements/markdown.py 65 2 97%
streamlit/elements/media.py 181 8 96%
streamlit/elements/metric.py 104 0 100%
streamlit/elements/pdf.py 49 2 96%
streamlit/elements/plotly_chart.py 129 6 95%
streamlit/elements/progress.py 36 0 100%
streamlit/elements/pyplot.py 39 2 95%
streamlit/elements/snow.py 10 0 100%
streamlit/elements/space.py 12 0 100%
streamlit/elements/spinner.py 44 3 93%
streamlit/elements/text.py 16 0 100%
streamlit/elements/toast.py 26 0 100%
streamlit/elements/vega_charts.py 238 3 99%
streamlit/elements/widgets/__init__.py 0 0 100%
streamlit/elements/widgets/audio_input.py 68 1 99%
streamlit/elements/widgets/button.py 245 6 98%
streamlit/elements/widgets/button_group.py 171 1 99%
streamlit/elements/widgets/camera_input.py 62 1 98%
streamlit/elements/widgets/chat.py 237 38 84%
streamlit/elements/widgets/checkbox.py 52 0 100%
streamlit/elements/widgets/color_picker.py 59 2 97%
streamlit/elements/widgets/data_editor.py 254 14 94%
streamlit/elements/widgets/file_uploader.py 108 10 91%
streamlit/elements/widgets/multiselect.py 114 5 96%
streamlit/elements/widgets/number_input.py 146 4 97%
streamlit/elements/widgets/radio.py 103 5 95%
streamlit/elements/widgets/select_slider.py 122 2 98%
streamlit/elements/widgets/selectbox.py 97 3 97%
streamlit/elements/widgets/slider.py 241 8 97%
streamlit/elements/widgets/text_widgets.py 130 6 95%
streamlit/elements/widgets/time_widgets.py 425 21 95%
streamlit/elements/write.py 166 20 88%
streamlit/emojis.py 4 0 100%
streamlit/env_util.py 21 3 86%
streamlit/error_util.py 33 2 94%
streamlit/errors.py 184 25 86%
streamlit/external/__init__.py 0 0 100%
streamlit/external/langchain/__init__.py 2 0 100%
streamlit/external/langchain/streamlit_callback_handler.py 141 82 42%
streamlit/file_util.py 84 8 90%
streamlit/git_util.py 100 5 95%
streamlit/logger.py 54 0 100%
streamlit/material_icon_names.py 1 0 100%
streamlit/navigation/__init__.py 0 0 100%
streamlit/navigation/page.py 78 2 97%
streamlit/net_util.py 55 3 95%
streamlit/platform.py 10 1 90%
streamlit/runtime/__init__.py 8 0 100%
streamlit/runtime/app_session.py 456 85 81%
streamlit/runtime/caching/__init__.py 21 0 100%
streamlit/runtime/caching/cache_data_api.py 191 3 98%
streamlit/runtime/caching/cache_errors.py 44 4 91%
streamlit/runtime/caching/cache_resource_api.py 165 1 99%
streamlit/runtime/caching/cache_type.py 11 1 91%
streamlit/runtime/caching/cache_utils.py 176 9 95%
streamlit/runtime/caching/cached_message_replay.py 108 1 99%
streamlit/runtime/caching/hashing.py 310 25 92%
streamlit/runtime/caching/legacy_cache_api.py 14 0 100%
streamlit/runtime/caching/storage/__init__.py 2 0 100%
streamlit/runtime/caching/storage/cache_storage_protocol.py 29 0 100%
streamlit/runtime/caching/storage/dummy_cache_storage.py 21 0 100%
streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py 67 1 99%
streamlit/runtime/caching/storage/local_disk_cache_storage.py 86 4 95%
streamlit/runtime/caching/ttl_cleanup_cache.py 28 0 100%
streamlit/runtime/connection_factory.py 96 11 89%
streamlit/runtime/context.py 137 0 100%
streamlit/runtime/context_util.py 18 0 100%
streamlit/runtime/credentials.py 139 4 97%
streamlit/runtime/download_data_util.py 27 0 100%
streamlit/runtime/forward_msg_cache.py 23 2 91%
streamlit/runtime/forward_msg_queue.py 63 4 94%
streamlit/runtime/fragment.py 112 2 98%
streamlit/runtime/media_file_manager.py 110 7 94%
streamlit/runtime/media_file_storage.py 15 0 100%
streamlit/runtime/memory_media_file_storage.py 73 0 100%
streamlit/runtime/memory_session_storage.py 15 0 100%
streamlit/runtime/memory_uploaded_file_manager.py 46 1 98%
streamlit/runtime/metrics_util.py 195 13 93%
streamlit/runtime/pages_manager.py 59 2 97%
streamlit/runtime/runtime.py 253 16 94%
streamlit/runtime/runtime_util.py 30 1 97%
streamlit/runtime/script_data.py 16 0 100%
streamlit/runtime/scriptrunner/__init__.py 5 0 100%
streamlit/runtime/scriptrunner/exec_code.py 49 5 90%
streamlit/runtime/scriptrunner/magic.py 83 1 99%
streamlit/runtime/scriptrunner/magic_funcs.py 10 1 90%
streamlit/runtime/scriptrunner/script_cache.py 27 0 100%
streamlit/runtime/scriptrunner/script_runner.py 235 27 89%
streamlit/runtime/scriptrunner_utils/__init__.py 0 0 100%
streamlit/runtime/scriptrunner_utils/exceptions.py 9 1 89%
streamlit/runtime/scriptrunner_utils/script_requests.py 106 5 95%
streamlit/runtime/scriptrunner_utils/script_run_context.py 118 0 100%
streamlit/runtime/secrets.py 241 25 90%
streamlit/runtime/session_manager.py 71 2 97%
streamlit/runtime/state/__init__.py 7 0 100%
streamlit/runtime/state/common.py 55 1 98%
streamlit/runtime/state/presentation.py 19 4 79%
streamlit/runtime/state/query_params.py 274 6 98%
streamlit/runtime/state/query_params_proxy.py 71 0 100%
streamlit/runtime/state/safe_session_state.py 77 9 88%
streamlit/runtime/state/session_state.py 503 36 93%
streamlit/runtime/state/session_state_proxy.py 62 8 87%
streamlit/runtime/state/widgets.py 19 0 100%
streamlit/runtime/stats.py 132 4 97%
streamlit/runtime/theme_util.py 46 1 98%
streamlit/runtime/uploaded_file_manager.py 39 3 92%
streamlit/runtime/websocket_session_manager.py 116 0 100%
streamlit/source_util.py 36 1 97%
streamlit/starlette.py 2 0 100%
streamlit/string_util.py 93 9 90%
streamlit/temporary_directory.py 18 1 94%
streamlit/testing/__init__.py 0 0 100%
streamlit/testing/v1/__init__.py 2 0 100%
streamlit/testing/v1/app_test.py 242 5 98%
streamlit/testing/v1/element_tree.py 1372 81 94%
streamlit/testing/v1/local_script_runner.py 71 2 97%
streamlit/testing/v1/util.py 17 0 100%
streamlit/time_util.py 28 1 96%
streamlit/type_util.py 148 16 89%
streamlit/url_util.py 39 4 90%
streamlit/user_info.py 105 8 92%
streamlit/util.py 38 1 97%
streamlit/version.py 3 0 100%
streamlit/watcher/__init__.py 3 0 100%
streamlit/watcher/event_based_path_watcher.py 184 25 86%
streamlit/watcher/folder_black_list.py 14 1 93%
streamlit/watcher/local_sources_watcher.py 127 9 93%
streamlit/watcher/path_watcher.py 42 3 93%
streamlit/watcher/polling_path_watcher.py 55 2 96%
streamlit/watcher/util.py 59 1 98%
streamlit/web/__init__.py 0 0 100%
streamlit/web/bootstrap.py 174 21 88%
streamlit/web/cache_storage_manager_config.py 5 0 100%
streamlit/web/cli.py 188 16 91%
streamlit/web/server/__init__.py 5 0 100%
streamlit/web/server/app_discovery.py 104 5 95%
streamlit/web/server/app_static_file_handler.py 29 3 90%
streamlit/web/server/authlib_tornado_integration.py 42 5 88%
streamlit/web/server/bidi_component_request_handler.py 65 8 88%
streamlit/web/server/browser_websocket_handler.py 147 18 88%
streamlit/web/server/component_file_utils.py 24 0 100%
streamlit/web/server/component_request_handler.py 55 4 93%
streamlit/web/server/media_file_handler.py 65 9 86%
streamlit/web/server/oauth_authlib_routes.py 162 33 80%
streamlit/web/server/oidc_mixin.py 46 0 100%
streamlit/web/server/routes.py 90 7 92%
streamlit/web/server/server.py 195 13 93%
streamlit/web/server/server_util.py 68 5 93%
streamlit/web/server/starlette/__init__.py 3 0 100%
streamlit/web/server/starlette/starlette_app.py 146 4 97%
streamlit/web/server/starlette/starlette_app_utils.py 101 7 93%
streamlit/web/server/starlette/starlette_auth_routes.py 233 51 78%
streamlit/web/server/starlette/starlette_gzip_middleware.py 30 0 100%
streamlit/web/server/starlette/starlette_routes.py 346 86 75%
streamlit/web/server/starlette/starlette_server.py 167 7 96%
streamlit/web/server/starlette/starlette_server_config.py 13 0 100%
streamlit/web/server/starlette/starlette_static_routes.py 64 3 95%
streamlit/web/server/starlette/starlette_websocket.py 203 23 89%
streamlit/web/server/stats_request_handler.py 59 5 92%
streamlit/web/server/upload_file_request_handler.py 59 7 88%
streamlit/web/server/websocket_headers.py 19 1 95%
TOTAL 23157 1594 93%

📊 View detailed coverage comparison

@github-actions github-actions bot added the do-not-merge PR is blocked from merging label Jan 26, 2026
@lukasmasuch lukasmasuch added ai-review If applied to PR or issue will run AI review workflow and removed do-not-merge PR is blocked from merging labels Jan 26, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Jan 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR enables st.select_slider to support dynamic option changes when a key is provided. The implementation converts the widget from index-based to string-based session state storage (using formatted option strings), following the established pattern used by st.radio, st.selectbox, and st.multiselect. This allows the widget to preserve its value when options change, as long as the selected option still exists in the new options list.

Key changes:

  • Add raw_value field to Slider.proto for string-based value storage
  • Refactor SelectSliderSerde to use formatted strings instead of indices
  • Set key_as_main_identity=True for stable widget identity during dynamic updates
  • Use string_array_value instead of double_array_value for session state
  • Add value validation functions to handle option changes gracefully
  • Update frontend to convert between string values and indices

Code Quality

Backend (Python):

  • The SelectSliderSerde refactoring from dataclass to class is well-structured with clear methods for serialization/deserialization
  • The new validate_and_sync_range_value_with_options function in options_selector_utils.py follows the existing pattern from validate_and_sync_value_with_options
  • Good use of format_func for value comparison, handling edge cases like custom objects without __eq__
  • The code in select_slider.py (lines 488-531) properly handles the distinction between single values and range values

Frontend (TypeScript):

  • The new helper functions stringValuesToIndices and indicesToStringValues are clean and well-placed at module level
  • The useExecuteWhenChanged hook usage (lines 203-220 in Slider.tsx) correctly handles the case when options change and indices need to be recomputed
  • State management properly distinguishes between select_slider and regular slider

Proto changes:

  • The addition of raw_value field (field 18) is backwards compatible
  • The // Next: 19 comment is a nice touch for future maintainability

Test Coverage

Unit tests are comprehensive:

  • test_select_slider_dynamic_options_preserves_valid_selection - Tests value preservation when option exists in both sets
  • test_select_slider_dynamic_options_range_resets_when_invalid - Tests range reset when values become invalid
  • test_select_slider_dynamic_options_with_format_func - Tests format_func interaction
  • test_select_slider_dynamic_options_with_enum - Tests enum value handling
  • test_select_slider_format_func_change_preserves_value - Regression test for format_func changes
  • test_raw_value_always_sent - Tests that raw_value is always populated

Frontend tests include:

  • sets widget value on mount using setStringArrayValue - Verifies string-based storage on mount
  • handles value changes with setStringArrayValue - Tests user interactions
  • handles range value changes with setStringArrayValue - Tests range slider interactions
  • reads rawValue from proto when available - Tests the new proto field

E2E tests cover:

  • test_dynamic_select_slider_props_and_options - Tests dynamic options with shared values
  • test_select_slider_range_dynamic_options_resets_on_invalid - Tests range reset behavior
  • test_format_func_change_preserves_value - Tests format_func change handling

The tests follow best practices including negative assertions (e.g., verifying values are NOT preserved when they shouldn't be).

Backwards Compatibility

The implementation is backwards compatible:

  1. Proto change is additive - The new raw_value field (18) doesn't affect existing fields
  2. Frontend handles missing rawValue gracefully - Falls back to default indices in getCurrStateFromProto
  3. Session state migration - The serde changes handle the transition from double_array_value to string_array_value

Note on enum coercion behavior change: The tests in radio_test.py and selectbox_test.py now expect enumCoercion: "off" to behave differently (assertions fail). This is documented and is a consequence of the string-based lookup approach - the deserialized value always comes from the current options, so class identity is correct when coercion is enabled.

Security & Risk

Low risk items identified:

  1. The bare except Exception: pass patterns (with # noqa: S110 comments) in the serialize method are intentional for handling format_func failures gracefully
  2. No security concerns identified - the changes are internal value handling improvements

Potential edge case: If two different options produce the same formatted string via format_func, the mapping will use the last occurrence. This could cause unexpected behavior, but it matches existing behavior in similar widgets.

Recommendations

No blocking issues found. A few minor observations:

  1. Consider adding a test for format_func collision - While this edge case is rare, documenting expected behavior when two options format to the same string would be helpful.

  2. Documentation note - The PR description mentions this follows the pattern from st.radio, st.selectbox, and st.multiselect. Consider adding a brief code comment in SelectSliderSerde referencing this pattern for future maintainers.

  3. Test comment improvement (optional) - In e2e_playwright/st_select_slider.py lines 162-165, the explanation of the test setup is excellent. Similar detailed comments in the Python unit tests would help future readers understand test intent.

Verdict

APPROVED: The PR is well-implemented with comprehensive test coverage, follows established patterns in the codebase, maintains backwards compatibility, and properly addresses the feature request for dynamic option changes in st.select_slider.


This is an automated AI review using opus-4.5-thinking. Please verify the feedback and use your judgment.

Comment on lines +329 to +338
# Use format_func comparison for all values. This correctly handles:
# - Custom objects without __eq__ (deepcopied instances)
# - Enum values (already from current class due to serde deserialization)
formatted_options_set = {format_func(o) for o in opt}
try:
formatted_value = format_func(current_value)
if formatted_value in formatted_options_set:
return current_value, False
except Exception: # noqa: S110
pass # format_func failed - value is invalid, fall through to reset
Copy link
Copy Markdown
Collaborator Author

@lukasmasuch lukasmasuch Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this recently for radio/selectbox, but I think we can keep this simpler and probably even more correct by just always using the formatted value here also for enum case (which is a super niche usecase anyways)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, OK, I guess we can see if someone reports an issue due to this change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, also ... I'm not even sure if the previous change was already rolled out

@streamlit streamlit deleted a comment from github-actions bot Jan 26, 2026
The Options prop tests were failing because they test select_slider
functionality but didn't specify type: SliderProto.Type.SELECT_SLIDER.
Without this, isSelectSlider() returns false and setDoubleArrayValue
is called instead of setStringArrayValue.

Co-Authored-By: Claude <[email protected]>
# options are not yet supported for dynamic changes
# keeping it at the same value:
options=["red", "orange", "yellow", "green", "blue"],
# "green" is at index 0 here (shared with initial options)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the (shared with initial option) part of this comment. Is this meant to describe how in this part of the test (initial state) the green option is at index 0? If so, I think this comment could be reworded to something like.

In the first part of the test, "green" is at index 0.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the comment to clarify it a bit

kwargs={"param": "initial kwarg param"},
# options are not yet supported for dynamic changes
# keeping it at the same value:
# "green" is at index 3 here (shared with updated options)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar here, unless I am misunderstanding something, maybe this would be clearer?

After update, "green" is at index 3.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated 👍

if (isSelectSlider(element)) {
// For select_slider, get string values and convert to indices
const stringValues = widgetMgr.getStringArrayValue(element)
if (stringValues === undefined) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Is this an error case, or just part of the normal lifecycle?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the normal lifecycle. It happens during initial render when no value has been stored in the widget manager yet. Added a clarifying comment.

- Update test comments to clearly describe option index positions
- Add comment explaining normal lifecycle case in getStateFromWidgetMgr

Co-Authored-By: Claude <[email protected]>
Comment on lines +658 to +659
def format_func(x):
return f"num_{x}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python Guide requires adding typing annotations to every new function, method or class member. The function parameter 'x' in the format_func definition is missing a type annotation. Add a type annotation such as 'def format_func(x: Any) -> str:' to comply with the typing requirements.

Suggested change
def format_func(x):
return f"num_{x}"
def format_func(x: Any) -> str:
return f"num_{x}"

Spotted by Graphite Agent (based on custom rule: Python Guide)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 👍

for i, s in enumerate(ui_value):
idx = self.formatted_option_to_index.get(s)
if idx is not None:
results.append((idx, self.options[idx]))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Maybe use .get() protectively in case the index is somehow out of range?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a defensive check to make sure that it cannot go out of range

results.append((default_idx, self.options[default_idx]))

if is_range and len(results) >= 2:
# Ensure start <= end by index
Copy link
Copy Markdown
Collaborator

@sfc-gh-lwilby sfc-gh-lwilby Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Maybe expand this and say, Ensure start <= end by returning deserialized range value in ascending order.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated 👍

assert "Selected: bravo" not in at.get("markdown")[-1].value

# Select "alpha" which exists in both option sets
at.select_slider[0].set_value("alpha").run()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This could be a bit better if there was another option shared between the arrays that could be selected here since it seems that alpha is the default as well.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated 👍

Copy link
Copy Markdown
Collaborator

@sfc-gh-lwilby sfc-gh-lwilby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just some minor comments.

- Add type annotation to format_func parameter (x: Any) -> str
- Add bounds check for index access in deserialize
- Expand comment to clarify ascending order requirement
- Improve test by using non-default shared option (delta)

Co-Authored-By: Claude <[email protected]>
@lukasmasuch lukasmasuch enabled auto-merge (squash) January 27, 2026 15:45
@lukasmasuch lukasmasuch merged commit f3561c7 into develop Jan 27, 2026
42 checks passed
@lukasmasuch lukasmasuch deleted the lukasmasuch/select-slider-dynamic branch January 27, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants