Repository: streamlit/streamlit
Pull request: Allow `None` as index for `st.radio`
## Describe your changes

This PR adds support for setting `None` as the index in `st.radio`. Using `index=None` will render an empty radio with no option selected which returns `None` as long as the user hasn't selected an option:

<img width="219" alt="image" src="https://github.com/streamlit/streamlit/assets/2852129/877bce2b-5d69-4184-888e-134da711c321">

- Spec: https://www.notion.so/snowflake-corp/Spec-v2-b00de8d56f99464693e7ddfdea0674e9

## GitHub Issue Link (if applicable)

- Closes #5220

## Testing Plan

- Unit Tests: ✅
- 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.


Problem statement:
Radio button without pre-selection
This option is possible as I saw in other platforms. It would be cool if you also add this option. 

---

Community voting on feature requests enables the Streamlit team to understand which features are most important to our users.

**If you'd like the Streamlit team to prioritize this feature request, please use the 👍 (thumbs up emoji) reaction in response to the initial post.**

Hints from issue discussion:
Yes, this would a convenient feature. I currently insert a blank element as the 1st radio button element, as a current workaround.
@kiansina Thanks for your suggestion! This sounds like a useful enhancement 👍  I will forward this feature request to our product team.

For other users reading this enhancement: please upvote the issue (👍) if you also want to have this implemented!
As a temporary solution I am using hacked checkboxes. No preselected box and only one can be picked at a time. Hope you find it useful 😄 .

```python
import streamlit as st

def disable_other_checkboxes(*other_checkboxes_keys):
    for checkbox_key in other_checkboxes_keys:
        st.session_state[checkbox_key] = False


option_1 = st.checkbox(
    "Option_1",
    key="op1",
    on_change=disable_other_checkboxes,
    args=("op2", "op3", "op4"),
)
option_2 = st.checkbox(
    "Option_2",
    key="op2",
    on_change=disable_other_checkboxes,
    args=("op1", "op3", "op4"),
)
option_3 = st.checkbox(
    "Option_3",
    key="op3",
    on_change=disable_other_checkboxes,
    args=("op2", "op1", "op4"),
)
option_4 = st.checkbox(
    "Option_4",
    key="op4",
    on_change=disable_other_checkboxes,
    args=("op2", "op3", "op1"),
)

st.write(option_1, option_2, option_3, option_4)
```
<img width="408" alt="Screenshot 2022-08-31 at 18 25 55" src="https://user-images.githubusercontent.com/41453558/187730074-8b8e095b-1ee9-4ccf-a6b5-550d79137127.png">

Can you all describe what exactly you want this for? This would help us decide if we want to support it or not.
My use case would be for a survey where we want to make sure users have responded to a question. I was planning on tracking if they've responded using session state but that doesn't account for the situation where the pre-selected option is what the user would actually like to respond to. 

Basically just trying to hack a way to get "required" questions when using `st.forms`
Hey all, just wanted to give a quick update. This issue is on our radar. The blocker for it is that (unfortunately!) the UI framework we use under the hood (BaseWeb) doesn’t support radio buttons without a default selection. So if we want to fix this, we’d need to either use a different UI framework (lots of work) or build our own component (also lots of work). It’s definitely possible we’ll do this at some point but for right now it seems like too big of a time investment.

Commit message: Use testid

Patch to review:
diff --git a/e2e/scripts/st_radio.py b/e2e/scripts/st_radio.py
index 586c292e5114..9d0edb0dc894 100644
--- a/e2e/scripts/st_radio.py
+++ b/e2e/scripts/st_radio.py
@@ -35,7 +35,7 @@
 i2 = st.radio("radio 2", options, 0, format_func=lambda x: x.capitalize())
 st.write("value 2:", i2)
 
-i3 = st.radio("radio 3", [])
+i3: None = st.radio("radio 3", [])
 st.write("value 3:", i3)
 
 i4 = st.radio("radio 4", options, disabled=True)
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-chromium].png
new file mode 100644
index 000000000000..711e80ddd432
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-firefox].png
new file mode 100644
index 000000000000..d8a6097ddf99
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-webkit].png
new file mode 100644
index 000000000000..3c9f81321ad6
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-chromium].png
new file mode 100644
index 000000000000..ff4033afdcbd
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-firefox].png
new file mode 100644
index 000000000000..f0e1ebdbb4dd
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-webkit].png
new file mode 100644
index 000000000000..9a62a2b5c2f2
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-callback_help[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-chromium].png
new file mode 100644
index 000000000000..4c8e4357e8d2
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-firefox].png
new file mode 100644
index 000000000000..7829720db9db
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-webkit].png
new file mode 100644
index 000000000000..b4f97479e34c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-chromium].png
new file mode 100644
index 000000000000..9630a0249ee7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-firefox].png
new file mode 100644
index 000000000000..ba83496d117b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-webkit].png
new file mode 100644
index 000000000000..569ef9cdd23a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-captions[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-chromium].png
new file mode 100644
index 000000000000..3bdebad7372a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-firefox].png
new file mode 100644
index 000000000000..9ef65932a495
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-webkit].png
new file mode 100644
index 000000000000..fcac2acd7250
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-chromium].png
new file mode 100644
index 000000000000..f5aa92c5f8b7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-firefox].png
new file mode 100644
index 000000000000..b56c2467cad7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-webkit].png
new file mode 100644
index 000000000000..8d877b397d85
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-collapsed_label[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..f805a75cb331
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..a9867b90634e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..42b5abe92a6e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-chromium].png
new file mode 100644
index 000000000000..a981752cc788
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-firefox].png
new file mode 100644
index 000000000000..7211513acbbf
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-webkit].png
new file mode 100644
index 000000000000..ff40f185d090
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-dataframe_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-chromium].png
new file mode 100644
index 000000000000..e36113325414
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-firefox].png
new file mode 100644
index 000000000000..ee40ba72ce4d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-webkit].png
new file mode 100644
index 000000000000..be7f32abfb45
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-chromium].png
new file mode 100644
index 000000000000..acf1e5f0393a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-firefox].png
new file mode 100644
index 000000000000..70ebd2e713cc
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-webkit].png
new file mode 100644
index 000000000000..485cdd5727de
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-default[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-chromium].png
new file mode 100644
index 000000000000..c44da3f3ab0b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-firefox].png
new file mode 100644
index 000000000000..7cb23e763aba
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-webkit].png
new file mode 100644
index 000000000000..f07c3db4d7c7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-chromium].png
new file mode 100644
index 000000000000..e5a6819fb08d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-firefox].png
new file mode 100644
index 000000000000..7102719d07cd
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-webkit].png
new file mode 100644
index 000000000000..778a72c08549
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-disabled[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-chromium].png
new file mode 100644
index 000000000000..700538c3e4a0
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-firefox].png
new file mode 100644
index 000000000000..957aa6f836e1
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-webkit].png
new file mode 100644
index 000000000000..87409a495362
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-chromium].png
new file mode 100644
index 000000000000..83ba0295979d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-firefox].png
new file mode 100644
index 000000000000..4bbd778f1ef6
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-webkit].png
new file mode 100644
index 000000000000..2cd84b0f8469
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-empty_selection[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..477d49cbc5b3
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..854a37bed918
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..c9da9afab634
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-chromium].png
new file mode 100644
index 000000000000..ac373d114aab
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-firefox].png
new file mode 100644
index 000000000000..841cd345b349
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-webkit].png
new file mode 100644
index 000000000000..19412a0becc1
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-formatted_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-chromium].png
new file mode 100644
index 000000000000..56d73249af22
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-firefox].png
new file mode 100644
index 000000000000..d87b843abb71
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-webkit].png
new file mode 100644
index 000000000000..f33e6e141d4b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-chromium].png
new file mode 100644
index 000000000000..8037c4f16b65
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-firefox].png
new file mode 100644
index 000000000000..8b2eb3e63a23
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-webkit].png
new file mode 100644
index 000000000000..4da991923a68
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-hidden_label[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-chromium].png
new file mode 100644
index 000000000000..7187f69ccb06
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-firefox].png
new file mode 100644
index 000000000000..339af51e0100
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-webkit].png
new file mode 100644
index 000000000000..71c65fbe6ae5
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-chromium].png
new file mode 100644
index 000000000000..2261e7818e9f
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-firefox].png
new file mode 100644
index 000000000000..88d0eb4234a9
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-webkit].png
new file mode 100644
index 000000000000..e148918e9984
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-chromium].png
new file mode 100644
index 000000000000..9f5069645627
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-firefox].png
new file mode 100644
index 000000000000..b6fc53fdf2af
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-webkit].png
new file mode 100644
index 000000000000..e3f9d6733aac
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-chromium].png
new file mode 100644
index 000000000000..da6a084320e6
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-firefox].png
new file mode 100644
index 000000000000..942d2e89973e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-webkit].png
new file mode 100644
index 000000000000..9ee7e1ee8d74
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-horizontal_captions[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..377dc0e7b47a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..28f17023a950
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..dd9f11004d16
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-chromium].png
new file mode 100644
index 000000000000..7ebf43d7b2de
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-firefox].png
new file mode 100644
index 000000000000..6a7154094669
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-webkit].png
new file mode 100644
index 000000000000..6091a673c05e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-markdown_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-chromium].png
new file mode 100644
index 000000000000..0bc4205e073d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-firefox].png
new file mode 100644
index 000000000000..a4073bfb1be9
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-webkit].png
new file mode 100644
index 000000000000..b73830076812
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-chromium].png
new file mode 100644
index 000000000000..e84b14c70d0a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-firefox].png
new file mode 100644
index 000000000000..802e0f7c9d1b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-webkit].png
new file mode 100644
index 000000000000..fe8b784c2ac0
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_radio_test/st_radio-no_options[light_theme-webkit].png differ
diff --git a/e2e_playwright/st_radio.py b/e2e_playwright/st_radio.py
new file mode 100644
index 000000000000..0ae28f3425f4
--- /dev/null
+++ b/e2e_playwright/st_radio.py
@@ -0,0 +1,97 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pandas as pd
+
+import streamlit as st
+from streamlit import runtime
+
+options = ("female", "male")
+markdown_options = (
+    "**bold text**",
+    "*italics text*",
+    "~strikethrough text~",
+    "shortcode: :blush:",
+    # link should not work in radio options
+    "[link text](www.example.com)",
+    "`code text`",
+    ":red[red] :blue[blue] :green[green] :violet[violet] :orange[orange]",
+)
+
+i1 = st.radio("radio 1 (default)", options)
+st.write("value 1:", i1)
+
+i2 = st.radio(
+    "radio 2 (Formatted options)",
+    options,
+    1,
+    format_func=lambda x: x.capitalize(),
+)
+st.write("value 2:", i2)
+
+i3: None = st.radio("radio 3 (no options)", [])
+st.write("value 3:", i3)
+
+i4 = st.radio("radio 4 (disabled)", options, disabled=True)
+st.write("value 4:", i4)
+
+i5 = st.radio("radio 5 (horizontal)", options, horizontal=True)
+st.write("value 5:", i5)
+
+i6 = st.radio("radio 6 (options from dataframe)", pd.DataFrame({"foo": list(options)}))
+st.write("value 6:", i6)
+
+i7 = st.radio("radio 7 (hidden label)", options, label_visibility="hidden")
+st.write("value 7:", i7)
+
+i8 = st.radio("radio 8 (collapsed label)", options, label_visibility="collapsed")
+st.write("value 8:", i8)
+
+i9 = st.radio("radio 9 (markdown options)", options=markdown_options)
+st.write("value 9:", i9)
+
+i10 = st.radio(
+    "radio 10 (with captions)",
+    ["A", "B", "C", "D", "E", "F", "G"],
+    captions=markdown_options,
+)
+st.write("value 10:", i10)
+
+i11 = st.radio(
+    "radio 11 (horizontal, captions)",
+    ["yes", "maybe", "no"],
+    captions=["Opt in", "", "Opt out"],
+    horizontal=True,
+)
+st.write("value 11:", i11)
+
+if runtime.exists():
+
+    def on_change():
+        st.session_state.radio_changed = True
+        st.text("Radio widget callback triggered")
+
+    st.radio(
+        "radio 12 (with callback, help)",
+        options,
+        1,
+        key="radio12",
+        on_change=on_change,
+        help="help text",
+    )
+    st.write("value 12:", st.session_state.radio12)
+    st.write("radio changed:", "radio_changed" in st.session_state)
+
+i13 = st.radio("radio 13 (empty selection)", options, index=None)
+st.write("value 13:", i13)
diff --git a/e2e_playwright/st_radio_test.py b/e2e_playwright/st_radio_test.py
new file mode 100644
index 000000000000..5aec97a8beb2
--- /dev/null
+++ b/e2e_playwright/st_radio_test.py
@@ -0,0 +1,124 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from playwright.sync_api import Page, expect
+
+from e2e_playwright.conftest import ImageCompareFunction
+
+
+def test_radio_widget_rendering(
+    themed_app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that the radio widgets are correctly rendered via screenshot matching."""
+    radio_widgets = themed_app.get_by_test_id("stRadio")
+    expect(radio_widgets).to_have_count(13)
+
+    assert_snapshot(radio_widgets.nth(0), name="st_radio-default")
+    assert_snapshot(radio_widgets.nth(1), name="st_radio-formatted_options")
+    assert_snapshot(radio_widgets.nth(2), name="st_radio-no_options")
+    assert_snapshot(radio_widgets.nth(3), name="st_radio-disabled")
+    assert_snapshot(radio_widgets.nth(4), name="st_radio-horizontal")
+    assert_snapshot(radio_widgets.nth(5), name="st_radio-dataframe_options")
+    assert_snapshot(radio_widgets.nth(6), name="st_radio-hidden_label")
+    assert_snapshot(radio_widgets.nth(7), name="st_radio-collapsed_label")
+    assert_snapshot(radio_widgets.nth(8), name="st_radio-markdown_options")
+    assert_snapshot(radio_widgets.nth(9), name="st_radio-captions")
+    assert_snapshot(radio_widgets.nth(10), name="st_radio-horizontal_captions")
+    assert_snapshot(radio_widgets.nth(11), name="st_radio-callback_help")
+    assert_snapshot(radio_widgets.nth(12), name="st_radio-empty_selection")
+
+
+def test_radio_has_correct_default_values(app: Page):
+    """Test that st.radio returns the correct initial values."""
+    markdown_elements = app.get_by_test_id("stMarkdown")
+    expect(markdown_elements).to_have_count(14)
+
+    expected = [
+        "value 1: female",
+        "value 2: male",
+        "value 3: None",
+        "value 4: female",
+        "value 5: female",
+        "value 6: female",
+        "value 7: female",
+        "value 8: female",
+        "value 9: bold text",
+        "value 10: A",
+        "value 11: yes",
+        "value 12: male",
+        "radio changed: False",
+        "value 13: None",
+    ]
+
+    for markdown_element, expected_text in zip(markdown_elements.all(), expected):
+        expect(markdown_element).to_have_text(expected_text, use_inner_text=True)
+
+
+def test_set_value_correctly_when_click(app: Page):
+    """Test that st.radio returns the correct values when the selection is changed."""
+    for index, element in enumerate(app.get_by_test_id("stRadio").all()):
+        if index != 3:  # skip disabled widget
+            element.locator('label[data-baseweb="radio"]').last.click(force=True)
+
+    expected = [
+        "value 1: male",
+        "value 2: male",
+        "value 3: None",
+        "value 4: female",
+        "value 5: male",
+        "value 6: male",
+        "value 7: male",
+        "value 8: male",
+        "value 9: red blue green violet orange",
+        "value 10: G",
+        "value 11: no",
+        "value 12: male",
+        "radio changed: False",
+        "value 13: male",
+    ]
+
+    for markdown_element, expected_text in zip(
+        app.get_by_test_id("stMarkdown").all(), expected
+    ):
+        expect(markdown_element).to_have_text(expected_text, use_inner_text=True)
+
+
+def test_calls_callback_on_change(app: Page):
+    """Test that it correctly calls the callback on change."""
+    radio_widget = app.get_by_test_id("stRadio").nth(11)
+
+    radio_widget.locator('label[data-baseweb="radio"]').first.click(force=True)
+
+    expect(app.get_by_test_id("stMarkdown").nth(11)).to_have_text(
+        "value 12: female",
+        use_inner_text=True,
+    )
+    expect(app.get_by_test_id("stMarkdown").nth(12)).to_have_text(
+        "radio changed: True",
+        use_inner_text=True,
+    )
+
+    # Change different date input to trigger delta path change
+    first_date_input_field = app.get_by_test_id("stRadio").first
+    first_date_input_field.locator('label[data-baseweb="radio"]').last.click(force=True)
+
+    expect(app.get_by_test_id("stMarkdown").first).to_have_text(
+        "value 1: male", use_inner_text=True
+    )
+
+    # Test if value is still correct after delta path change
+    expect(app.get_by_test_id("stMarkdown").nth(11)).to_have_text(
+        "value 12: female",
+        use_inner_text=True,
+    )
diff --git a/frontend/lib/src/components/elements/Markdown/Markdown.tsx b/frontend/lib/src/components/elements/Markdown/Markdown.tsx
index c0a932c4f327..00bab744812e 100644
--- a/frontend/lib/src/components/elements/Markdown/Markdown.tsx
+++ b/frontend/lib/src/components/elements/Markdown/Markdown.tsx
@@ -37,7 +37,7 @@ export default function Markdown({
 }: MarkdownProps): ReactElement {
   const styleProp = { width }
   return (
-    <div className="stMarkdown" style={styleProp}>
+    <div className="stMarkdown" style={styleProp} data-testid="stMarkdown">
       {element.help ? (
         <StyledLabelHelpWrapper>
           <StreamlitMarkdown
diff --git a/frontend/lib/src/components/shared/Radio/Radio.test.tsx b/frontend/lib/src/components/shared/Radio/Radio.test.tsx
index 6a28638fd83e..8bbc19dcfe1d 100644
--- a/frontend/lib/src/components/shared/Radio/Radio.test.tsx
+++ b/frontend/lib/src/components/shared/Radio/Radio.test.tsx
@@ -101,6 +101,7 @@ describe("Radio widget", () => {
     const radioOptions = screen.getAllByRole("radio")
     expect(radioOptions).toHaveLength(3)
 
+    // @ts-expect-error
     const checked = radioOptions[props.value]
     expect(checked).toBeChecked()
   })
diff --git a/frontend/lib/src/components/shared/Radio/Radio.tsx b/frontend/lib/src/components/shared/Radio/Radio.tsx
index c01abe44d02a..92083af79b2c 100644
--- a/frontend/lib/src/components/shared/Radio/Radio.tsx
+++ b/frontend/lib/src/components/shared/Radio/Radio.tsx
@@ -32,7 +32,7 @@ export interface Props {
   horizontal: boolean
   theme: EmotionTheme
   width?: number
-  value: number
+  value: number | null
   onChange: (selectedIndex: number) => any
   options: any[]
   captions: any[]
@@ -46,12 +46,12 @@ interface State {
    * The value specified by the user via the UI. If the user didn't touch this
    * widget's UI, the default value is used.
    */
-  value: number
+  value: number | null
 }
 
 class Radio extends React.PureComponent<Props, State> {
   public state: State = {
-    value: this.props.value,
+    value: this.props.value ?? null,
   }
 
   public componentDidUpdate(prevProps: Props): void {
@@ -61,7 +61,7 @@ class Radio extends React.PureComponent<Props, State> {
       this.props.value !== this.state.value
     ) {
       this.setState((_, prevProps) => {
-        return { value: prevProps.value }
+        return { value: prevProps.value ?? null }
       })
     }
   }
@@ -110,7 +110,9 @@ class Radio extends React.PureComponent<Props, State> {
         </WidgetLabel>
         <RadioGroup
           onChange={this.onChange}
-          value={this.state.value.toString()}
+          value={
+            this.state.value !== null ? this.state.value.toString() : undefined
+          }
           disabled={disabled}
           align={horizontal ? ALIGN.horizontal : ALIGN.vertical}
           aria-label={label}
diff --git a/frontend/lib/src/components/widgets/Radio/Radio.test.tsx b/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
index 50dd53dcc914..12410be47527 100644
--- a/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
+++ b/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
@@ -91,6 +91,7 @@ describe("Radio widget", () => {
     const radioOptions = screen.getAllByRole("radio")
     expect(radioOptions).toHaveLength(3)
 
+    // @ts-expect-error
     const checked = radioOptions[props.element.default]
     expect(checked).toBeChecked()
   })
@@ -192,6 +193,7 @@ describe("Radio widget", () => {
     props.widgetMgr.submitForm("form")
 
     // Our widget should be reset, and the widgetMgr should be updated
+    // @ts-expect-error
     const defaultValue = radioOptions[props.element.default]
     expect(defaultValue).toBeChecked()
 
diff --git a/frontend/lib/src/components/widgets/Radio/Radio.tsx b/frontend/lib/src/components/widgets/Radio/Radio.tsx
index ee17cd4b9068..dddb729c7408 100644
--- a/frontend/lib/src/components/widgets/Radio/Radio.tsx
+++ b/frontend/lib/src/components/widgets/Radio/Radio.tsx
@@ -36,7 +36,7 @@ interface State {
    * The value specified by the user via the UI. If the user didn't touch this
    * widget's UI, the default value is used.
    */
-  value: number
+  value: number | null
 }
 
 class Radio extends React.PureComponent<Props, State> {
@@ -46,11 +46,11 @@ class Radio extends React.PureComponent<Props, State> {
     value: this.initialValue,
   }
 
-  get initialValue(): number {
+  get initialValue(): number | null {
     // If WidgetStateManager knew a value for this widget, initialize to that.
     // Otherwise, use the default value from the widget protobuf.
     const storedValue = this.props.widgetMgr.getIntValue(this.props.element)
-    return storedValue !== undefined ? storedValue : this.props.element.default
+    return storedValue ?? this.props.element.default ?? null
   }
 
   public componentDidMount(): void {
@@ -79,7 +79,7 @@ class Radio extends React.PureComponent<Props, State> {
   private updateFromProtobuf(): void {
     const { value } = this.props.element
     this.props.element.setValue = false
-    this.setState({ value }, () => {
+    this.setState({ value: value ?? null }, () => {
       this.commitWidgetValue({ fromUi: false })
     })
   }
@@ -100,7 +100,7 @@ class Radio extends React.PureComponent<Props, State> {
   private onFormCleared = (): void => {
     this.setState(
       (_, prevProps) => {
-        return { value: prevProps.element.default }
+        return { value: prevProps.element.default ?? null }
       },
       () => this.commitWidgetValue({ fromUi: true })
     )
diff --git a/lib/streamlit/elements/widgets/radio.py b/lib/streamlit/elements/widgets/radio.py
index 51b89e303b49..c5a1db0aa394 100644
--- a/lib/streamlit/elements/widgets/radio.py
+++ b/lib/streamlit/elements/widgets/radio.py
@@ -12,9 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import annotations
+
 from dataclasses import dataclass
 from textwrap import dedent
-from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Sequence, cast
+from typing import TYPE_CHECKING, Any, Callable, Generic, Sequence, cast, overload
 
 from streamlit.elements.form import current_form_id
 from streamlit.elements.utils import (
@@ -51,46 +53,89 @@
 @dataclass
 class RadioSerde(Generic[T]):
     options: Sequence[T]
-    index: int
+    index: int | None
+
+    def serialize(self, v: object) -> int | None:
+        if v is None:
+            return None
 
-    def serialize(self, v: object) -> int:
-        if len(self.options) == 0:
-            return 0
-        return index_(self.options, v)
+        return 0 if len(self.options) == 0 else index_(self.options, v)
 
     def deserialize(
         self,
-        ui_value: Optional[int],
+        ui_value: int | None,
         widget_id: str = "",
-    ) -> Optional[T]:
+    ) -> T | None:
         idx = ui_value if ui_value is not None else self.index
 
         return (
             self.options[idx]
-            if len(self.options) > 0 and self.options[idx] is not None
+            if idx is not None
+            and len(self.options) > 0
+            and self.options[idx] is not None
             else None
         )
 
 
 class RadioMixin:
-    @gather_metrics("radio")
+    @overload
     def radio(
         self,
         label: str,
         options: OptionSequence[T],
         index: int = 0,
         format_func: Callable[[Any], Any] = str,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only args:
+        disabled: bool = False,
+        horizontal: bool = False,
+        captions: Sequence[str] | None = None,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T:
+        pass
+
+    @overload
+    def radio(
+        self,
+        label: str,
+        options: OptionSequence[T],
+        index: None = None,
+        format_func: Callable[[Any], Any] = str,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only args:
+        disabled: bool = False,
+        horizontal: bool = False,
+        captions: Sequence[str] | None = None,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T | None:
+        pass
+
+    @gather_metrics("radio")
+    def radio(
+        self,
+        label: str,
+        options: OptionSequence[T],
+        index: int | None = 0,
+        format_func: Callable[[Any], Any] = str,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only args:
         disabled: bool = False,
         horizontal: bool = False,
-        captions: Optional[Sequence[str]] = None,
+        captions: Sequence[str] | None = None,
         label_visibility: LabelVisibility = "visible",
-    ) -> Optional[T]:
+    ) -> T | None:
         r"""Display a radio button widget.
 
         Parameters
@@ -126,8 +171,10 @@ def radio(
             described in the ``label`` parameter and will be cast to str
             internally by default. For pandas.DataFrame, the first column is
             selected.
-        index : int
-            The index of the preselected option on first render.
+        index : int or None
+            The index of the preselected option on first render. If ``None``,
+            will initialize empty and return ``None`` until the user selects an option.
+            Defaults to 0 (the first option).
         format_func : function
             Function to modify the display of radio options. It receives
             the raw option as an argument and should output the label to be
@@ -166,7 +213,7 @@ def radio(
         Returns
         -------
         any
-            The selected option.
+            The selected option or ``None`` if no option is selected.
 
         Example
         -------
@@ -186,6 +233,22 @@ def radio(
            https://doc-radio.streamlit.app/
            height: 300px
 
+        To initialize an empty radio widget, use ``None`` as the index value:
+
+        >>> import streamlit as st
+        >>>
+        >>> genre = st.radio(
+        ...     "What's your favorite movie genre",
+        ...     [":rainbow[Comedy]", "***Drama***", "Documentary :movie_camera:"],
+        ...     index=None,
+        ... )
+        >>>
+        >>> st.write("You selected:", genre)
+
+        .. output::
+           https://doc-radio-empty.streamlit.app/
+           height: 300px
+
         """
         ctx = get_script_run_ctx()
         return self._radio(
@@ -209,20 +272,20 @@ def _radio(
         self,
         label: str,
         options: OptionSequence[T],
-        index: int = 0,
+        index: int | None = 0,
         format_func: Callable[[Any], Any] = str,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only args:
         disabled: bool = False,
         horizontal: bool = False,
         label_visibility: LabelVisibility = "visible",
-        captions: Optional[Sequence[str]] = None,
-        ctx: Optional[ScriptRunContext],
-    ) -> Optional[T]:
+        captions: Sequence[str] | None = None,
+        ctx: ScriptRunContext | None,
+    ) -> T | None:
         key = to_key(key)
         check_callback_rules(self.dg, on_change)
         check_session_state_rules(default_value=None if index == 0 else index, key=key)
@@ -243,17 +306,17 @@ def _radio(
             page=ctx.page_script_hash if ctx else None,
         )
 
-        if not isinstance(index, int):
+        if not isinstance(index, int) and index is not None:
             raise StreamlitAPIException(
                 "Radio Value has invalid type: %s" % type(index).__name__
             )
 
-        if len(opt) > 0 and not 0 <= index < len(opt):
+        if index is not None and len(opt) > 0 and not 0 <= index < len(opt):
             raise StreamlitAPIException(
                 "Radio index must be between 0 and length of options"
             )
 
-        def handle_captions(caption: Optional[str]) -> str:
+        def handle_captions(caption: str | None) -> str:
             if caption is None:
                 return ""
             elif isinstance(caption, str):
@@ -266,7 +329,8 @@ def handle_captions(caption: Optional[str]) -> str:
         radio_proto = RadioProto()
         radio_proto.id = id
         radio_proto.label = label
-        radio_proto.default = index
+        if index is not None:
+            radio_proto.default = index
         radio_proto.options[:] = [str(format_func(option)) for option in opt]
         radio_proto.form_id = current_form_id(self.dg)
         radio_proto.horizontal = horizontal
@@ -296,7 +360,10 @@ def handle_captions(caption: Optional[str]) -> str:
         )
 
         if widget_state.value_changed:
-            radio_proto.value = serde.serialize(widget_state.value)
+            if widget_state.value is not None:
+                serialized_value = serde.serialize(widget_state.value)
+                if serialized_value is not None:
+                    radio_proto.value = serialized_value
             radio_proto.set_value = True
 
         self.dg._enqueue("radio", radio_proto)
diff --git a/lib/streamlit/testing/element_tree.py b/lib/streamlit/testing/element_tree.py
index 7e5787435524..2e870ce6ade6 100644
--- a/lib/streamlit/testing/element_tree.py
+++ b/lib/streamlit/testing/element_tree.py
@@ -62,6 +62,7 @@
 T = TypeVar("T")
 
 
+@dataclass
 class InitialValue:
     """This class is used to represent the initial value of a widget."""
 
@@ -594,7 +595,7 @@ def decrement(self) -> NumberInput:
 
 @dataclass(repr=False)
 class Radio(Widget, Generic[T]):
-    _value: T | None
+    _value: T | None | InitialValue
 
     proto: RadioProto
     options: list[str]
@@ -603,7 +604,7 @@ class Radio(Widget, Generic[T]):
     def __init__(self, proto: RadioProto, root: ElementTree):
         self.proto = proto
         self.root = root
-        self._value = None
+        self._value = InitialValue()
 
         self.type = "radio"
         self.id = proto.id
@@ -616,20 +617,22 @@ def __init__(self, proto: RadioProto, root: ElementTree):
         self.key = user_key_from_widget_id(self.id)
 
     @property
-    def index(self) -> int:
+    def index(self) -> int | None:
+        if self.value is None:
+            return None
         return self.options.index(str(self.value))
 
     @property
-    def value(self) -> T:
+    def value(self) -> T | None:
         """The currently selected value from the options."""
-        if self._value is not None:
+        if not isinstance(self._value, InitialValue):
             return self._value
         else:
             state = self.root.session_state
             assert state
             return cast(T, state[self.id])
 
-    def set_value(self, v: T) -> Radio[T]:
+    def set_value(self, v: T | None) -> Radio[T]:
         self._value = v
         return self
 
@@ -640,7 +643,8 @@ def widget_state(self) -> WidgetState:
         """
         ws = WidgetState()
         ws.id = self.id
-        ws.int_value = self.index
+        if self.index is not None:
+            ws.int_value = self.index
         return ws
 
 
diff --git a/lib/tests/streamlit/elements/radio_test.py b/lib/tests/streamlit/elements/radio_test.py
index 2d23c149806f..48aabff2261c 100644
--- a/lib/tests/streamlit/elements/radio_test.py
+++ b/lib/tests/streamlit/elements/radio_test.py
@@ -23,6 +23,7 @@
 import streamlit as st
 from streamlit.errors import StreamlitAPIException
 from streamlit.proto.LabelVisibilityMessage_pb2 import LabelVisibilityMessage
+from streamlit.testing.script_interactions import InteractiveScriptTests
 from tests.delta_generator_test_case import DeltaGeneratorTestCase
 
 
@@ -41,6 +42,7 @@ def test_just_label(self):
         )
         self.assertEqual(c.default, 0)
         self.assertEqual(c.disabled, False)
+        self.assertEqual(c.HasField("default"), True)
         self.assertEqual(c.captions, [])
 
     def test_just_disabled(self):
@@ -50,6 +52,17 @@ def test_just_disabled(self):
         c = self.get_delta_from_queue().new_element.radio
         self.assertEqual(c.disabled, True)
 
+    def test_none_value(self):
+        """Test that it can be called with None as index value."""
+        st.radio("the label", ("m", "f"), index=None)
+
+        c = self.get_delta_from_queue().new_element.radio
+        self.assertEqual(c.label, "the label")
+        # If a proto property is null is not determined by this value,
+        # but by the check via the HasField method:
+        self.assertEqual(c.default, 0)
+        self.assertEqual(c.HasField("default"), False)
+
     def test_horizontal(self):
         """Test that it can be called with horizontal param."""
         st.radio("the label", ("m", "f"), horizontal=True)
@@ -235,3 +248,28 @@ def test_some_captions(self):
         self.assertEqual(c.label, "the label")
         self.assertEqual(c.default, 0)
         self.assertEqual(c.captions, ["first caption", "", "", "last caption"])
+
+
+class RadioInteractiveTest(InteractiveScriptTests):
+    def test_radio_interaction(self):
+        """Test interactions with an empty radio widget."""
+        script = self.script_from_string(
+            """
+        import streamlit as st
+
+        st.radio("the label", ("m", "f"), index=None)
+        """
+        )
+        sr = script.run()
+        radio = sr.radio[0]
+        assert radio.value is None
+
+        # Select option m
+        sr2 = radio.set_value("m").run()
+        radio = sr2.radio[0]
+        assert radio.value == "m"
+
+        # # Clear the value
+        sr3 = radio.set_value(None).run()
+        radio = sr3.radio[0]
+        assert radio.value is None
diff --git a/proto/streamlit/proto/Radio.proto b/proto/streamlit/proto/Radio.proto
index d88e9ebe8eea..95abb238f78d 100644
--- a/proto/streamlit/proto/Radio.proto
+++ b/proto/streamlit/proto/Radio.proto
@@ -21,11 +21,11 @@ import "streamlit/proto/LabelVisibilityMessage.proto";
 message Radio {
   string id = 1;
   string label = 2;
-  int32 default = 3;
+  optional int32 default = 3;
   repeated string options = 4;
   string help = 5;
   string form_id = 6;
-  int32 value = 7;
+  optional int32 value = 7;
   bool set_value = 8;
   bool disabled = 9;
   bool horizontal = 10;


Code review comments:
diff_hunk: @@ -51,46 +53,89 @@
 @dataclass
 class RadioSerde(Generic[T]):
     options: Sequence[T]
-    index: int
+    index: int | None
+
+    def serialize(self, v: object) -> int | None:
+        if v is None:
+            return None
 
-    def serialize(self, v: object) -> int:
-        if len(self.options) == 0:
-            return 0
-        return index_(self.options, v)
+        return 0 if len(self.options) == 0 else index_(self.options, v)
 
     def deserialize(
         self,
-        ui_value: Optional[int],
+        ui_value: int | None,
         widget_id: str = "",
-    ) -> Optional[T]:
+    ) -> T | None:
         idx = ui_value if ui_value is not None else self.index
 
         return (
             self.options[idx]
-            if len(self.options) > 0 and self.options[idx] is not None
+            if idx is not None
+            and len(self.options) > 0
+            and self.options[idx] is not None
             else None
         )
 
 
 class RadioMixin:
-    @gather_metrics("radio")
+    @overload
     def radio(
         self,
         label: str,
         options: OptionSequence[T],
         index: int = 0,
         format_func: Callable[[Any], Any] = str,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only args:
+        disabled: bool = False,
+        horizontal: bool = False,
+        captions: Sequence[str] | None = None,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T:
+        pass
+
+    @overload
+    def radio(
+        self,
+        label: str,
+        options: OptionSequence[T],
+        index: None = None,
+        format_func: Callable[[Any], Any] = str,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only args:
+        disabled: bool = False,
+        horizontal: bool = False,
+        captions: Sequence[str] | None = None,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T | None:
+        pass
path: lib/streamlit/elements/widgets/radio.py
text: @user1:
Are these duplicates necessary?

@author:
Not strictly, but this signature overload improves typing for users since radio cannot return `None` if index was not `None`. It's not a big problem for radio, since radio was already annotated with `Optional`. But for some of the other widgets, some users may run into typing issues if we just add `None` to the return types without overloading. 

@author:
It's a small typing benefit for users that has the cost of some redundancy... but I'm not sure if it is worth it.

@user1:
Not sure where I land on whether its worth it, but could you add a comment if you choose to leave it?

@author:
I decided to remove the overloads for radio and selectbox since they cannot fully cover the cases anyways (since empty list also returns `None` as mentioned by Vincent below).

diff_hunk: @@ -51,46 +53,89 @@
 @dataclass
 class RadioSerde(Generic[T]):
     options: Sequence[T]
-    index: int
+    index: int | None
+
+    def serialize(self, v: object) -> int | None:
+        if v is None:
+            return None
 
-    def serialize(self, v: object) -> int:
-        if len(self.options) == 0:
-            return 0
-        return index_(self.options, v)
+        return 0 if len(self.options) == 0 else index_(self.options, v)
 
     def deserialize(
         self,
-        ui_value: Optional[int],
+        ui_value: int | None,
         widget_id: str = "",
-    ) -> Optional[T]:
+    ) -> T | None:
         idx = ui_value if ui_value is not None else self.index
 
         return (
             self.options[idx]
-            if len(self.options) > 0 and self.options[idx] is not None
+            if idx is not None
+            and len(self.options) > 0
+            and self.options[idx] is not None
             else None
         )
 
 
 class RadioMixin:
-    @gather_metrics("radio")
+    @overload
     def radio(
         self,
         label: str,
         options: OptionSequence[T],
         index: int = 0,
         format_func: Callable[[Any], Any] = str,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only args:
+        disabled: bool = False,
+        horizontal: bool = False,
+        captions: Sequence[str] | None = None,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T:
path: lib/streamlit/elements/widgets/radio.py
text: @user1:
Hm, is the return type here correct? I think there's a case where `st.radio` is passed an empty options sequence and unspecified index (which thus defaults to 0) where the return type of the call to `st.radio` will be `None`.

This will probably be fine most of the time as `T` will often be unbound, but a user could have explicitly typed the `OptionsSequence` doing something like the following

```python
my_options: List[str] = []

# Some conditional logic which doesn't append anything to `my_options`
# In this script run
if False:
    my_options.append("option 1")

choice = st.radio('pick an option', my_options)
```

In the code snippet above, I think `mypy` will complain that a `str` is not returned by
`st.radio`, but I would consider the above code valid.

I think the other overload + the signature used in the function implementation itself should be enough to cover all cases.

@author:
Oh, you are right. Empty option list also returns `None` and there doesn't seem to be a good way to type an empty list. So, I think we can just remove the overload since it always needs to be typed with `None` & the type of the options.

diff_hunk: @@ -51,46 +53,89 @@
 @dataclass
 class RadioSerde(Generic[T]):
     options: Sequence[T]
-    index: int
+    index: int | None
+
+    def serialize(self, v: object) -> int | None:
+        if v is None:
+            return None
 
-    def serialize(self, v: object) -> int:
-        if len(self.options) == 0:
-            return 0
-        return index_(self.options, v)
+        return 0 if len(self.options) == 0 else index_(self.options, v)
 
     def deserialize(
         self,
-        ui_value: Optional[int],
+        ui_value: int | None,
         widget_id: str = "",
-    ) -> Optional[T]:
+    ) -> T | None:
         idx = ui_value if ui_value is not None else self.index
 
         return (
             self.options[idx]
-            if len(self.options) > 0 and self.options[idx] is not None
+            if idx is not None
+            and len(self.options) > 0
+            and self.options[idx] is not None
             else None
         )
 
 
 class RadioMixin:
-    @gather_metrics("radio")
+    @overload
     def radio(
         self,
         label: str,
         options: OptionSequence[T],
         index: int = 0,
         format_func: Callable[[Any], Any] = str,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only args:
+        disabled: bool = False,
+        horizontal: bool = False,
+        captions: Sequence[str] | None = None,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T:
+        pass
+
+    @overload
+    def radio(
+        self,
+        label: str,
+        options: OptionSequence[T],
+        index: None = None,
+        format_func: Callable[[Any], Any] = str,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only args:
+        disabled: bool = False,
+        horizontal: bool = False,
+        captions: Sequence[str] | None = None,
+        label_visibility: LabelVisibility = "visible",
+    ) -> T | None:
path: lib/streamlit/elements/widgets/radio.py
text: @user1:
Hm, I think in this case the return type could probably be narrowed further to `None` since we know that we can't get back a `T` if `index is None`.

@author:
Once the user has selected a value, it won't return `None` anymore. So I think it still needs to have both return types.