Skip to content

Allow dynamic changes to st.radio options when key is provided#13611

Merged
lukasmasuch merged 9 commits intodevelopfrom
feature/key-as-main-identity-for-radio-options
Jan 21, 2026
Merged

Allow dynamic changes to st.radio options when key is provided#13611
lukasmasuch merged 9 commits intodevelopfrom
feature/key-as-main-identity-for-radio-options

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Jan 16, 2026

Describe your changes

Allow dynamically changing the options for st.radio 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.

This also applies a needed change from index-based widget value to string-based (formatted options). The same change was already implemented for st.selectbox and st.multiselect some time ago.

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.

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Jan 16, 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 16, 2026

✅ PR preview is ready!

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

@lukasmasuch lukasmasuch added security-assessment-completed change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Jan 16, 2026
@lukasmasuch lukasmasuch marked this pull request as ready for review January 16, 2026 17:30
@lukasmasuch lukasmasuch requested a review from a team as a code owner January 16, 2026 17:30
Copilot AI review requested due to automatic review settings January 16, 2026 17:30
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 PR enables dynamic option changes for st.radio when a key is provided by migrating from index-based to string-based widget values. The change aligns with the approach previously implemented for st.selectbox and st.multiselect, allowing options to change without triggering widget identity changes while gracefully handling invalid values.

Changes:

  • Migrated st.radio from index-based to string-based widget values (formatted options)
  • Modified widget identity computation to use key_as_main_identity=True instead of whitelisting specific parameters
  • Updated validation logic to reset to default when selected value is removed from options

Reviewed changes

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

Show a summary per file
File Description
proto/streamlit/proto/Radio.proto Deprecated value field and added raw_value field for string-based values
lib/streamlit/elements/widgets/radio.py Refactored RadioSerde to use string-based serialization/deserialization with formatted options mapping
lib/streamlit/testing/v1/element_tree.py Updated AppTest to send string values instead of int values for radio widgets
frontend/lib/src/components/widgets/Radio/Radio.tsx Changed widget to use string values and convert to/from index for UIRadio component
frontend/lib/src/components/widgets/Radio/Radio.test.tsx Updated all tests to expect setStringValue calls instead of setIntValue
lib/tests/streamlit/elements/radio_test.py Added comprehensive tests for dynamic options behavior and removed obsolete whitelisting tests
e2e_playwright/st_radio.py Updated test app to demonstrate dynamic options with value preservation and reset scenarios
e2e_playwright/st_radio_test.py Enhanced e2e test to verify both value preservation and reset behaviors

cursor[bot]

This comment was marked as outdated.

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

Summary

This PR enables dynamic option changes for st.radio when a key is provided, aligning its behavior with st.selectbox and st.multiselect. The implementation switches from index-based to string-based widget values, which provides more robust handling of option changes. Key changes include:

  • Backend: New RadioSerde class using string-based values, key_as_main_identity=True for stable widget IDs
  • Frontend: Changed from setIntValue/getIntValue to setStringValue/getStringValue
  • Protobuf: Added new raw_value field (13), deprecated old value field (7)
  • Behavior: Selected value persists if it exists in new options; resets to default otherwise

Code Quality

Python Backend (lib/streamlit/elements/widgets/radio.py)

The implementation is well-structured and follows existing patterns from selectbox:

  1. RadioSerde class (lines 70-118): Clean serialization/deserialization logic using string-based values. Good error handling and edge case coverage for empty options and None values.

  2. Element ID computation (lines 406-418): Correctly uses key_as_main_identity=True to ensure stable widget identity when key is provided.

  3. Value validation (lines 487-495): Properly uses validate_and_sync_value_with_options to handle the case where selected value is no longer in options.

  4. Value type registration (line 477): Correctly changed to value_type="string_value".

Frontend (frontend/lib/src/components/widgets/Radio/Radio.tsx)

  1. Clean implementation: The component properly converts between string values and indices internally (lines 68-84).

  2. Memoization: Good use of useMemo for the selectedIndex computation.

  3. Callback stability: onChange callback has proper dependencies.

Protobuf (proto/streamlit/proto/Radio.proto)

  1. Backwards compatible: Old value field properly marked [deprecated = true] (line 32), new raw_value field added (line 33).

  2. Documentation: Good "Next: 14" comment for future field numbering.

Testing Tree (lib/streamlit/testing/v1/element_tree.py)

The Radio widget's _widget_state property correctly uses string_value instead of int_value (line 999).

Test Coverage

Python Unit Tests (lib/tests/streamlit/elements/radio_test.py)

Excellent coverage with:

  1. test_stable_id_with_key (lines 323-368): Verifies widget ID remains stable when key is provided.

  2. test_unstable_id_without_key (lines 370-387): Verifies widget ID changes when no key is provided and options change.

  3. test_dynamic_options_with_key_retains_value (lines 468-503): Tests value preservation when selected value exists in new options.

  4. test_dynamic_options_with_key_resets_invalid_value (lines 506-541): Tests reset to default when value no longer exists.

  5. test_dynamic_options_with_key_and_none_index (lines 544-577): Tests behavior with index=None.

  6. Updated enum coercion test (lines 413-450): Correctly reflects that string-based lookup now preserves enum types.

E2E Tests (e2e_playwright/st_radio_test.py)

Well-designed tests covering:

  1. Selection reset when value is removed from options (lines 242-258)
  2. Selection preservation when value exists in both option sets (lines 261-277)
  3. Good docstrings explaining the test scenarios
  4. Proper use of expect for auto-wait assertions

Frontend Unit Tests (frontend/lib/src/components/widgets/Radio/Radio.test.tsx)

All tests correctly updated to use setStringValue instead of setIntValue with appropriate assertions.

Backwards Compatibility

This change is backwards compatible:

  1. Protobuf: The old value field (int32) is preserved and marked deprecated. New raw_value field added. Old frontends will still work with new backends.

  2. Python API: No changes to the public API. Same parameters, same return values.

  3. Session State: Existing code using st.session_state with radio widgets will continue to work - values are still the actual option values, not indices.

  4. Potential behavioral change: One subtle change - previously options and format_func were considered "whitelisted" kwargs that would cause widget ID to change even with a key. Now they are not (which enables the dynamic options feature). This is the intended behavior and matches selectbox/multiselect.

Security & Risk

No security concerns identified.

Low regression risk:

  • The change is well-tested across all layers (unit, integration, E2E)
  • The pattern is already proven in st.selectbox and st.multiselect
  • Backwards compatibility with protobuf is maintained

Minor considerations:

  • format_func changes may cause unexpected behavior: If the user changes format_func to produce different strings for the same value, the selection may reset. The E2E test comment at line 146-149 in st_radio.py acknowledges this limitation.

Recommendations

  1. Consider adding a note to documentation: The docstring could mention that when using dynamic options with a key, the selection is preserved only if the value (after format_func) exists in the new options.

  2. Test script updates (lines 460, 545, 984 in script_runner_test.py): These are correctly updated to use string_value instead of int_value. Good attention to detail.

  3. Snapshot updates: Multiple snapshot images were updated which is expected given the test scenarios changed.

Verdict

APPROVED: This is a well-implemented feature that brings st.radio to parity with st.selectbox and st.multiselect. The code is clean, well-tested, backwards compatible, and follows established patterns in the codebase. The PR correctly addresses the related issue #11277 by enabling dynamic option changes when a key is provided.


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

option_index = index_(self.options, v)
return self.formatted_options[option_index]
except ValueError:
# Value is a string that's not in options (shouldn't happen for radio)
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] Does this happen sometimes when the format string changes?

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've refactored this code to fix an issue with custom objects without eq (similar to PR #13648). The old code path that caught ValueError from index_() no longer exists.

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.

Does it make sense to add a test with the format string changing?

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

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 questions about the format string handling.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 21, 2026

📈 Frontend coverage change detected

The frontend unit test (vitest) coverage has increased by 0.0600%

  • Current PR: 86.4600% (13347 lines, 1807 missed)
  • Latest develop: 86.4000% (13339 lines, 1814 missed)

🎉 Great job on improving test coverage!

📊 View detailed coverage comparison

@lukasmasuch
Copy link
Copy Markdown
Collaborator Author

@cursor review

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR switches st.radio state to string-based values (formatted labels) with key as the main identity, adds raw_value in Radio.proto, updates the React widget to use string values, and expands Python/e2e tests and snapshots for dynamic option behavior.

Code Quality

  • Potential UI state desync when format_func changes: the backend only sends raw_value when value_needs_reset or value_changed, which ignores format changes; the frontend computes selection via options.indexOf(value) so stale values can map to no selection. See lib/streamlit/elements/widgets/radio.py lines 503-511 and frontend/lib/src/components/widgets/Radio/Radio.tsx lines 77-84.

Test Coverage

  • Good additions: Python tests for dynamic options and custom objects, and e2e coverage for dynamic updates.
  • Missing best-practice items: new Python tests lack type annotations (e.g., lib/tests/streamlit/elements/radio_test.py lines 468-582) and the e2e dynamic test could add a negative assertion (e.g., after toggle, ensure "banana" is not selected) per the e2e guidelines (e2e_playwright/st_radio_test.py lines 246-280).

Backwards Compatibility

  • Switching to string-valued widget state can change behavior when formatted labels are not unique or format_func changes; duplicate formatted labels now collapse to a single value (UI sends label string). Consider whether this is acceptable or add a guard/warning. Related logic in frontend/lib/src/components/widgets/Radio/Radio.tsx lines 68-83.
  • Radio.proto deprecates value in favor of raw_value (proto/streamlit/proto/Radio.proto lines 32-33). If you expect older frontends/cached clients, consider temporarily populating both fields or adding a fallback to preserve compatibility.

Security & Risk

No security issues found. Main risk is a UX regression where selection appears cleared after format_func changes even though server state remains valid.

Recommendations

  1. Ensure raw_value is sent when the formatted label changes (even if the underlying option is still valid), so the frontend selection stays in sync; consider comparing serde.serialize(current_value) to the incoming UI value and set set_value when they differ.
  2. Add an e2e assertion that the previously selected option is not selected after removal, to satisfy the “must NOT happen” guideline.
  3. Add type annotations to the new Python tests in radio_test.py per the unit test guide.
  4. Decide on a strategy for duplicate formatted labels (guard, warning, or documented limitation) and document the behavior.

Verdict

CHANGES REQUESTED: The format_func-change path can desynchronize UI selection from server state and should be addressed before merge.


This is an automated AI review using gpt-5.2-codex-high. Please verify the feedback and use your judgment.

@github-actions github-actions bot added the do-not-merge PR is blocked from merging label Jan 21, 2026
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation and removed do-not-merge PR is blocked from merging change:feature PR contains new feature or enhancement implementation labels Jan 21, 2026
@lukasmasuch lukasmasuch merged commit 93f74da into develop Jan 21, 2026
53 of 55 checks passed
@lukasmasuch lukasmasuch deleted the feature/key-as-main-identity-for-radio-options branch January 21, 2026 22:18
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.

4 participants