Skip to content

Conversation

@lukasmasuch
Copy link
Collaborator

@lukasmasuch lukasmasuch commented Dec 23, 2025

Describe your changes

Allow dynamically changing the options for st.multiselect without triggering an identity change / state reset. If the current selected options isn't in the list of available option, it will be reset to the default value.

GitHub Issue Link (if applicable)

Testing Plan

  • Added e2e and unit 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.

@snyk-io
Copy link
Contributor

snyk-io bot commented Dec 23, 2025

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.

@sfc-gh-lmasuch sfc-gh-lmasuch added security-assessment-completed Security assessment has been completed for PR change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Dec 23, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Dec 23, 2025

✅ PR preview is ready!

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

@lukasmasuch lukasmasuch changed the title Allow dynamic changes to multiselect options when key is provided Allow dynamic changes to multiselect options when key is provided Jan 5, 2026
@lukasmasuch lukasmasuch marked this pull request as ready for review January 5, 2026 19:17
Copilot AI review requested due to automatic review settings January 5, 2026 19:17
Copy link
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 dynamic changes to st.multiselect options when a key is provided, maintaining widget identity and preserving valid selections while filtering out invalid ones when the options list changes.

Key changes:

  • Removed options and format_func from the identity computation when a key is provided, allowing these to change without resetting widget state
  • Added validation logic to filter multiselect values against current options, preserving valid selections and removing invalid ones
  • Refactored selectbox validation logic into reusable utility functions

Reviewed changes

Copilot reviewed 7 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
lib/streamlit/elements/widgets/multiselect.py Modified identity computation to exclude options and format_func from whitelisted params; added validation logic to filter invalid selections when options change
lib/streamlit/elements/widgets/selectbox.py Refactored validation logic to use new validate_value_against_options utility function
lib/streamlit/elements/lib/options_selector_utils.py Added two new validation functions: validate_value_against_options for selectbox and validate_multiselect_value_against_options for multiselect
lib/tests/streamlit/elements/multiselect_test.py Updated stable ID tests to reflect new whitelisted params; added three new AppTest tests covering option expansion, filtering, and complete removal scenarios
lib/tests/streamlit/elements/lib/options_selector_utils_test.py Added comprehensive parameterized tests for both new validation utility functions
e2e_playwright/st_multiselect_test.py Enhanced test_dynamic_multiselect_props to test both filtering and preservation behaviors with improved documentation
e2e_playwright/st_multiselect.py Updated test app to change both options and format_func between initial and updated states; changed accept_new_options to False consistently to avoid identity changes

@lukasmasuch
Copy link
Collaborator Author

@cursor review

Copy link

@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.

✅ Bugbot reviewed your changes and found no bugs!

@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Jan 5, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Jan 5, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 5, 2026

Summary

This PR enables dynamic changes to st.multiselect options when a key is provided, without resetting the widget state entirely. When options change dynamically:

  • Valid selections are preserved: Values that exist in both old and new option sets are kept
  • Invalid selections are filtered out: Values no longer in the new options are removed
  • The widget identity remains stable (no re-mount/state loss)

The implementation introduces two new validation utility functions that are shared between selectbox and multiselect widgets, improving code reusability.

Key Changes:

  1. Added validate_value_against_options() and validate_multiselect_value_against_options() in options_selector_utils.py
  2. Updated multiselect.py to use the new validation when accept_new_options=False
  3. Refactored selectbox.py to use the shared validation function
  4. Removed options and format_func from the key_as_main_identity set for multiselect

Code Quality

Strengths:

  • Clean separation of concerns with validation logic extracted to reusable utility functions
  • Well-documented functions with numpydoc-style docstrings
  • Proper type annotations throughout
  • Code follows existing patterns in the codebase
  • Good use of existing index_() function for value comparison (handles float epsilon)
  • Appropriate use of reset_state_value() to avoid the "cannot be modified after widget instantiated" error

Minor Observations:

  1. options_selector_utils.py:367 - The # noqa: PERF203 comment acknowledges a potential performance concern with exceptions in loops. This is acceptable for typical use cases with small option sets, but worth noting for users with large selections.

  2. multiselect.py:552-565 - The logic correctly skips validation when accept_new_options=True, preserving user-entered values that aren't in the original options list.

  3. selectbox.py:599-609 - Good refactoring to use the shared validation function, reducing code duplication.

Test Coverage

Unit Tests:

  • ✅ Comprehensive parameterized tests for validate_value_against_options() covering:

    • Value in options (no reset)
    • None value handling
    • Value not in options (reset to default)
    • Custom default index
    • None default index
    • Empty options
    • Numeric and float values
  • ✅ Comprehensive parameterized tests for validate_multiselect_value_against_options() covering:

    • All values in options
    • Empty values list
    • Some values not in options (partial filtering)
    • All values not in options (complete reset to empty)
    • Numeric value types
  • ✅ Three new AppTest integration tests:

    • test_multiselect_preserves_selection_when_options_expand
    • test_multiselect_filters_invalid_selections
    • test_multiselect_resets_when_all_selections_removed

E2E Tests:

  • ✅ Updated test_dynamic_multiselect_props with clear documentation of test scenarios
  • ✅ Tests selection reset when values are removed from options
  • ✅ Tests selection preservation when values exist in both option sets
  • ✅ Updated snapshots for the new behavior

Test Quality Notes:

  • E2E tests follow best practices: use expect for auto-wait, label-based locators, and helper utilities
  • Tests have descriptive docstrings explaining the test scenarios
  • Good use of select_for_multiselect and expect_prefixed_markdown utilities

Backwards Compatibility

This change is backwards compatible.

  • Existing apps using st.multiselect with a key will experience the new filtering behavior, which is an improvement (previously, dynamic option changes would cause state issues)
  • Apps without a key continue to work as before (widget identity is content-based)
  • The accept_new_options=True path is unchanged - user-entered values are still preserved
  • The validation only kicks in when options change AND the widget has an existing value from session state

Migration Note: Users relying on the previous behavior where changing options would reset state might see different behavior now - selections that remain valid will be preserved. This is the intended fix for issues #7855 and #11277.

Security & Risk

No security concerns identified.

  • No new external inputs or user data handling
  • The validation functions only compare values against provided options
  • Session state updates use the existing reset_state_value() API designed for this purpose

Risk Assessment:

  • Low risk: The changes are localized to option validation logic
  • The implementation gracefully handles edge cases (empty options, None values, empty selections)
  • The existing index_() function handles float comparison correctly

Recommendations

The PR is well-implemented and ready for merge. A few minor suggestions for consideration:

  1. Consider adding a note about format_func in the documentation: The E2E test comments explain that changing format_func may cause selections to be filtered if the formatted label no longer matches. This behavior might be worth documenting for users.

  2. Test coverage for session state key update: While the logic updates session state when key is not None, the unit tests pass None for the key parameter. Consider adding a test that verifies session state is actually updated when a key is provided.

  3. Consider edge case: When format_func changes between renders, the validation compares raw values (not formatted), which is correct. However, if users have complex objects as options, they should ensure their objects implement proper equality. This is existing behavior but worth noting.

Verdict

APPROVED: This PR provides a well-tested, clean implementation that fixes a real user pain point (#7855, #11277). The code follows existing patterns, has comprehensive test coverage, and is backwards compatible. The validation logic is correctly shared between selectbox and multiselect widgets.


This is an automated AI review. Please verify the feedback and use your judgment.

)


def validate_value_against_options(
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion (non-blocking): Because these functions validate and also have a possible side-effect of changing state, consider either:

  1. Giving them either a name that makes the potential for side-effects clearer.
  2. Refactoring this into 2 different functions and have the widget layer compose the functions so that we can keep validation logic pure.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Renamed the function to make it a bit more explicit

@lukasmasuch lukasmasuch enabled auto-merge (squash) January 9, 2026 14:00
@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

📉 Frontend coverage change detected

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

  • Current PR: 86.5200% (12812 lines, 1727 missed)
  • Latest develop: 86.5600% (12812 lines, 1721 missed)

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

📊 View detailed coverage comparison

@lukasmasuch lukasmasuch merged commit 4eaf69f into develop Jan 9, 2026
44 checks passed
@lukasmasuch lukasmasuch deleted the feature/dynamic-multiselect-options branch January 9, 2026 15:03
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 security-assessment-completed Security assessment has been completed for PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

st.multiselect returns empty on dynamically changing options.

4 participants