[feature] Add required parameter to st.pills and st.segmented_control#14414
[feature] Add required parameter to st.pills and st.segmented_control#14414lukasmasuch merged 6 commits intodevelopfrom
required parameter to st.pills and st.segmented_control#14414Conversation
Adds `required: bool = False` parameter that prevents deselection in single-select mode when True. Raises StreamlitAPIException when used with multi-select mode. Includes type narrowing via @overload. Co-Authored-By: Claude Opus 4.6 <[email protected]>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ PR preview is ready!
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Pull request overview
Adds a new required parameter to Streamlit’s button-group widgets (st.pills, st.segmented_control) to support non-deselectable single-select behavior, with corresponding backend validation, proto wiring, typing overloads, and test coverage across Python, frontend, and E2E.
Changes:
- Extend
ButtonGroupprotobuf + Python widget plumbing to carryrequiredand rejectrequired=Truewithselection_mode="multi". - Add Python typing overloads and typing tests to narrow return types when
required=Trueand a non-Nonedefault is provided. - Update the frontend ButtonGroup widget behavior and add unit + E2E tests for required/no-deselect flows.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| proto/streamlit/proto/ButtonGroup.proto | Adds required field to the ButtonGroup proto so the flag can reach the frontend. |
| lib/streamlit/elements/widgets/button_group.py | Threads required through widget creation, adds validation, and introduces overloads/doc updates for the public API. |
| lib/tests/streamlit/elements/button_group_test.py | Adds Python unit tests verifying proto propagation and backend validation for required. |
| lib/tests/streamlit/typing/pills_types.py | Adds mypy typing assertions for pills return-type narrowing with required. |
| lib/tests/streamlit/typing/segmented_control_types.py | Adds mypy typing assertions for segmented_control return-type narrowing with required. |
| frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx | Implements required-mode no-deselect logic in the widget click handling. |
| frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx | Adds frontend unit tests covering required vs not-required single-select interactions. |
| e2e_playwright/st_pills.py | Adds a pills demo section for required behavior to the E2E app script. |
| e2e_playwright/st_pills_test.py | Adds E2E coverage for required pills behavior. |
| e2e_playwright/st_segmented_control.py | Adds a segmented control demo section for required behavior to the E2E app script. |
| e2e_playwright/st_segmented_control_test.py | Adds E2E coverage for required segmented control behavior. |
Comments suppressed due to low confidence (2)
lib/streamlit/elements/widgets/button_group.py:333
- The single-select overloads restrict
requiredtoLiteral[True]/Literal[False]. That prevents valid calls likerequired=is_requiredwhereis_required: boolis not a literal, because no overload matches. Add an overload (or relax the existing one) that acceptsrequired: bool = Falsefor single-select so non-literal booleans type-check while still keeping theLiteral[True]+defaultnarrowing overload.
label: str,
options: OptionSequence[V],
*,
selection_mode: Literal["single"] = "single",
default: V | None = None,
required: Literal[False] = ...,
format_func: Callable[[Any], str] | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
lib/streamlit/elements/widgets/button_group.py:657
- Same overload issue as
pills:segmented_controlsingle-select overloads only acceptrequiredasLiteral[True]/Literal[False], sorequired=some_boolwon’t match any overload. Add arequired: bool = Falsesingle-select overload (returningV | None) to keep the API usable with non-literal booleans while preserving the narrowing case.
def segmented_control(
self,
label: str,
options: OptionSequence[V],
*,
selection_mode: Literal["single"] = "single",
default: V | None = None,
required: Literal[False] = ...,
format_func: Callable[[Any], str] | None = None,
key: str | int | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx
Outdated
Show resolved
Hide resolved
…uired=true When required=true and the user clicks an already-selected option, skip the setValueWithSource call to avoid triggering an unnecessary backend rerun. The test is also updated to verify that no widget update is sent in this case. Co-Authored-By: Claude Opus 4.6 <[email protected]>
SummaryThis PR adds a Changes span all layers: protobuf definition, Python backend, TypeScript frontend, Python unit tests, TypeScript unit tests, mypy type tests, and E2E tests. Code QualityAll three reviewers agree the implementation is clean, well-structured, and follows existing Streamlit codebase conventions.
No blocking code-quality issues found by any reviewer. Test CoverageAll reviewers rate test coverage as excellent/comprehensive across all layers:
Backwards CompatibilityUnanimous agreement: Fully backwards compatible. The Security & RiskUnanimous agreement: No security concerns. Changes are confined to widget UI state management — no new endpoints, routes, auth, CORS, or external requests. No new dependencies added. Regression risk is low. External test recommendation
AccessibilityUnanimous agreement: No accessibility regressions. The change modifies click behavior only and does not alter DOM structure, roles, labeling, or focus behavior. All reviewers agree existing accessibility posture is maintained. Reviewer Agreement & DisagreementsFull agreement across all three reviewers:
No disagreements identifiedAll three reviewers independently reached the same conclusions on all major assessment areas. Complementary (non-conflicting) recommendations:
Recommendations
None of these are blocking. VerdictAPPROVED: All three reviewers unanimously approve. The implementation is clean, well-tested across all layers (proto, Python, TypeScript, E2E, type tests), fully backwards compatible, and poses no security, accessibility, or regression risk. The type narrowing via overloads is a nice developer experience improvement. This is a consolidated AI review by 📋 Review by `gemini-3.1-pro`SummaryAdds a Code QualityThe code is well-structured and follows Streamlit's conventions. The frontend implementation correctly prevents unnecessary state updates and backend reruns when a user clicks an already-selected option with Test CoverageTest coverage is excellent:
Backwards CompatibilityThe changes are fully backwards compatible. The Security & RiskNo security concerns or regression risks identified. The changes are confined to the widget's UI state management and do not interact with sensitive areas like routing, auth, or cross-origin behaviour. External test recommendation
AccessibilityThe changes do not negatively impact accessibility. While standard HTML form validation ( RecommendationsNo required changes. The implementation is solid. One minor observation: In the multi-select overload for both VerdictAPPROVED: The implementation is clean, well-tested, and backwards compatible. This is an automated AI review by 📋 Review by `gpt-5.3-codex-high`SummaryThis PR adds a new Code QualityThe implementation is clean and consistent with existing patterns:
No blocking code-quality issues found. Test CoverageCoverage is strong and appropriately layered for this change:
I did not run tests in this review (per instruction), but test additions are comprehensive for the changed behavior. Backwards CompatibilityBackwards compatibility looks good:
No breaking API changes identified. Security & RiskNo security-sensitive surfaces were modified (no auth/session/cookie/CORS/routing/upload/path handling changes). External test recommendation
AccessibilityNo accessibility regressions identified from this change:
Recommendations
VerdictAPPROVED: The implementation is coherent, well-tested across layers, backward compatible, and ready to merge. This is an automated AI review by 📋 Review by `opus-4.6-thinking`SummaryThis PR adds a Changes span all layers: protobuf definition, Python backend, TypeScript frontend, Python unit tests, TypeScript unit tests, mypy type tests, and E2E tests. Code QualityThe implementation is clean, well-structured, and follows existing patterns in the codebase. Protobuf ( Python backend (
TypeScript frontend (
Test CoverageTest coverage is thorough across all layers: Python unit tests (
TypeScript unit tests (
Type tests (
E2E tests (
Minor observation: The E2E AGENTS.md recommends preferring aggregated scenario tests over micro-tests when they share the same page state. The three tests per widget ( Backwards CompatibilityFully backwards compatible. The Security & RiskNo security concerns identified. The changes are:
Regression risk is low. The External test recommendation
AccessibilityNo accessibility concerns. The Recommendations
VerdictAPPROVED: Clean, well-tested feature addition that is fully backwards compatible, follows existing codebase patterns, and poses no security or regression risk. The type narrowing via overloads is a nice developer experience improvement. This is an automated AI review by |
Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add typing comments documenting runtime validation for required=True with selection_mode="multi" - Consolidate E2E tests into aggregated scenario tests to reduce browser loads - Add aria-required attribute for screen reader accessibility Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Consolidated Review — required parameter for st.pills and st.segmented_control
Summary
This PR adds a required: bool = False parameter to st.pills and st.segmented_control that prevents deselection in single-select mode. The implementation spans backend (Python), frontend (TypeScript/React), protobuf, type overloads, and comprehensive tests across all layers (Python unit, TypeScript unit, typing, and E2E).
Reviewer Consensus
| Reviewer | Verdict | Key Observations |
|---|---|---|
| gemini-3.1-pro | APPROVED | Clean implementation, excellent test coverage, proper accessibility |
| gpt-5.3-codex-high | CHANGES REQUESTED | Raised one correctness concern about clearable=True + bind="query-params" + required=True type contract |
| opus-4.6-thinking | APPROVED | Thorough coverage, good frontend optimization, no blocking issues |
All three reviewers agreed on:
- Code quality is high and follows existing Streamlit patterns
- Test coverage is thorough across all layers (Python unit, TS unit, typing, E2E)
- Backwards compatibility is fully preserved (
requireddefaults toFalse) - No security concerns or regression risks
- Accessibility is handled correctly (
aria-requiredattribute) - No external test coverage needed
- The
@overloadsignatures are well-structured with correct precedence
Point of disagreement:
One reviewer (gpt-5.3-codex-high) raised a concern about the -> V type guarantee being potentially violated when bind="query-params" is used alongside required=True and a non-None default. The widget's _button_group method unconditionally passes clearable=True to register_widget, meaning an empty query parameter could theoretically seed the widget to None at registration time, breaking the type contract. The other two reviewers did not flag this.
Resolution: After independent verification, this concern is valid but not blocking. The clearable=True is pre-existing code (not introduced by this PR), and the scenario requires a specific three-way parameter combination (required=True + default=V + bind="query-params") combined with URL manipulation. The established codebase pattern (used by selectbox, radio, etc.) is to conditionally set clearable based on whether the widget can be in an empty state. This should be addressed — either in this PR or as a tracked follow-up — but it does not block the core feature, which works correctly for all standard usage patterns. This is captured as an inline comment.
Code Quality
The implementation is clean and idiomatic:
- The
requiredparameter is correctly excluded fromcompute_and_register_element_id, consistent with howdisabledis handled. - The frontend optimization of skipping state updates via reference equality (returning the same array from
handleSelection) prevents unnecessary backend reruns. - The runtime validation for
required=True+selection_mode="multi"provides a clear, actionable error message. - Docstrings follow the established NumPy-style convention.
Test Coverage
Test coverage is comprehensive and follows repository guidelines:
- Python unit tests:
RequiredParameterTestcovers default values, proto field assignment, required-with-default, multi-select rejection, and required=False+multi-select allowance. Uses@parameterized.expandfor bothst.pillsandst.segmented_control. - TypeScript unit tests: Cover deselection prevention, selection changes, deselection when not required, and
aria-requiredattribute presence/absence. Includes negative assertion (toHaveBeenCalledTimes(1)) confirmingsetStringArrayValueis not called unnecessarily. - Type tests: Verify return type narrowing for all relevant combinations. Document the known limitation that
required=True+selection_mode="multi"cannot be caught at type-check time. - E2E tests: Aggregated scenario tests reduce browser loads, covering required-with-default, required-without-default, and not-required baselines for both widgets.
Verdict
APPROVED — Well-implemented feature with thorough test coverage, correct accessibility handling, full backwards compatibility, and no security concerns. The clearable interaction with bind="query-params" is a valid edge case noted inline that should be tracked for follow-up.
Consolidated review by opus-4.6-thinking from 3 of 3 expected models (gemini-3.1-pro, gpt-5.3-codex-high, opus-4.6-thinking). No models failed to complete review.
This review also includes 1 inline comment(s) on specific code lines.
The spec document is no longer needed as part of this feature branch. Co-Authored-By: Claude Opus 4.6 <[email protected]>

Describe your changes
Adds
required: bool = Falseparameter tost.pillsandst.segmented_control:Prevents deselection in single-select mode when
required=True(clicking already-selected option does nothing)Raises
StreamlitAPIExceptionwhenrequired=Truewithselection_mode="multi"Provides return type narrowing via
@overloadwhenrequired=Trueanddefaultis set (returnsVinstead ofV | None)Product spec
GitHub Issue Link (if applicable)
Testing Plan
lib/tests/streamlit/elements/button_group_test.py— Testsrequiredparameter behaviorfrontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx— Tests deselection preventionlib/tests/streamlit/typing/pills_types.py,segmented_control_types.py— Tests return type narrowinge2e_playwright/st_pills_test.py,st_segmented_control_test.py— Tests required behaviorContribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.
Agent metrics