One tap, one value: Radio makes 'exactly one option' the UI's problem, not your validation's.
A Dropdown makes users click twice — open, then pick. Radio lays every option out and guarantees exactly one lands in your function.
gr.Radio(choices=[...]) renders a list of mutually exclusive radio buttons — select one, the previous selection clears. Your listener receives exactly the chosen entry: a string, a number, or (with type="index") the zero-based position. Choices can be plain values or (display name, value) tuples, so you can show "Pro" and receive 9. The component carries label, info, value (preselect), visible, interactive, and fires three events: .select, .change and .input. Preprocess hands the choice through verbatim and raises a clear Error if a value isn't among the choices; postprocess passes strings and numbers straight back out.
Every ML demo hits the same moment: the user must pick one inference mode, one model, one plan. Dropdowns hide the options behind a click; Checkboxes allow nonsense states like 'both'; free text allows typos. Radio is the honest control — all options visible, exactly one selected, and the value arriving in Python already clean. That last part is the quiet win: because preprocess validates against the choice list, a bad value can't sneak into your function. Six options or fewer, and Radio is almost always the right input.
import gradio as gr
def sentiment(review: str, model: str) -> str:
vibe = "positive" if "love" in review.lower() else "neutral"
return f"[{model}] {vibe} ({len(review.split())} words)"
demo = gr.Interface(
sentiment,
inputs=[
gr.Textbox(label="Review", lines=3, placeholder="Paste a review..."),
gr.Radio(["fast", "accurate"], value="fast", label="Model"),
],
outputs=gr.Textbox(label="Verdict"),
)
if __name__ == "__main__":
demo.launch(prevent_thread_lock=True)* Running on local URL: http://127.0.0.1:7860 * To create a public link, set `share=True` in `launch()`. Rendered UI: an Interface with a 3-line Textbox "Review" (placeholder "Paste a review...") stacked above a Radio "Model" with the options fast and accurate — "fast" pre-selected because of value="fast" — a Submit button, and a "Verdict" Textbox below. Pasting "I love this keyboard" (4 words) and hitting Submit sends the string and "fast" to Python; Verdict shows "[fast] positive (4 words)". Picking "accurate" and re-running changes the prefix — one click, no reopening a dropdown.
Executed with gradio 6.18.0: the app built, launched and printed exactly the two output lines shown. The click-through result follows from the function body — the UI contract (Radio passes the selected string straight in) was verified in the same run.
import gradio as gr
PLANS = [("Starter", 0), ("Pro", 9), ("Team", 29)]
with gr.Blocks() as demo:
plan = gr.Radio(PLANS, label="Plan", info="Billed monthly")
seats = gr.Number(label="Seats", value=1, minimum=1)
quote = gr.Markdown()
payload = gr.JSON(label="Last select event")
def price(p, n, evt: gr.SelectData):
name, base = p
return f"**{name}: EUR {base * n}/mo**", {"index": evt.index, "value": evt.value}
plan.select(price, [plan, seats], [quote, payload])
if __name__ == "__main__":
demo.launch(prevent_thread_lock=True)* Running on local URL: http://127.0.0.1:7860 * To create a public link, set `share=True` in `launch()`. Rendered UI: a vertical Blocks stack — Radio "Plan" with helper text "Billed monthly" and three options Starter, Pro, Team (none pre-selected, since no value= was passed), a Number "Seats" at 1, an empty Markdown quote area, and a JSON panel labeled "Last select event". The displayed names come from the tuples, but your listener receives the raw values. Clicking "Pro" with 3 seats fires .select once: quote renders "Pro: EUR 27/mo" and the JSON panel shows the event payload — evt.value is the chosen entry's value (9) and evt.index its zero-based position (1).
Executed with gradio 6.18.0: built and launched cleanly. Tuple choices are documented in the installed choices docstring ("name is the displayed name ... value is the value to be passed"), and evt.value/evt.index match the gr.SelectData contract shipped in the same install.
import gradio as gr
SIZES = {"iris": ["50 rows", "150 rows"], "titanic": ["100 rows", "891 rows"]}
with gr.Blocks() as demo:
dataset = gr.Dropdown(["iris", "titanic"], label="Dataset")
sample = gr.Radio(label="Sample size")
head = gr.JSON(label="Preview")
def pick_sizes(ds):
return gr.Radio(choices=SIZES[ds], value=SIZES[ds][-1])
def preview(ds, size):
n = int(size.split()[0])
return {"rows": n, "columns": 12 if ds == "titanic" else 4}
dataset.change(pick_sizes, dataset, sample)
sample.change(preview, [dataset, sample], head)
if __name__ == "__main__":
demo.launch(prevent_thread_lock=True)* Running on local URL: http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
Rendered UI: a Dropdown "Dataset", an empty Radio "Sample size" beneath it, and a JSON "Preview" panel. Choosing "titanic" rebuilds the Radio in place with choices 100 rows / 891 rows, pre-selecting "891 rows"; the Radio returned from pick_sizes carries both the new choices and the new value in a single update. Clicking "100 rows" fires .change and Preview shows {"rows": 100, "columns": 12}. Switching to "iris" swaps the options to 50 rows / 150 rows — dependent choices, no duplicated UI.
Executed with gradio 6.18.0 (preview rendered via gr.JSON — gr.Dataframe needed a jinja2 upgrade this box doesn't have; same demo logic). The return-gr.Radio-update pattern is the standard way to rebuild dependent options.
| Flag | Meaning |
|---|---|
choices=[...] | Strings, numbers, or (display name, value) tuples — the UI shows the name, your function gets the value. |
value=... | Preselect an option; pass a callable and Gradio recomputes the default on every app load. Leave None for a clean start. |
type="value" | "index" | "value" (default) returns the choice itself; "index" returns its zero-based position — verified: choices ["a","b"] preprocess "b" to 1 in 6.18.0. |
info=... | Small markdown helper line under the label — put the 'why' here ('Billed monthly') and keep label the 'what'. |
rtl=True | Renders the options right-to-left — matters for Arabic/Hebrew layouts where default order reads backwards. |
buttons=[gr.Button(...)] | Mount custom gr.Button() instances in the component's top-right toolbar; their .click() listeners fire from there (PR #12539, merged Dec 2025). |
Radio belongs to Gradio's original input set, documented alongside Textbox, Checkbox, Dropdown and Slider from the earliest releases. The file gradio/components/radio.py was created in the June 2023 refactor that split components into separate modules (PR #4487) — the commit history shows just 33 commits touching it since, one of the most stable surfaces in the library.
For most of its life Radio rendered exactly one thing: the option list. PR #12539 ("Add ability to add custom buttons to components", merged 2025-12-16) gave components a shared toolbar, and Radio's buttons= parameter now accepts gr.Button() instances that appear top-right and trigger their own .click() events — a per-component action row without any layout code.
When the user clicks an option, the Svelte frontend marks exactly one radio in the group and sends the raw value over the WebSocket/HTTP queue to Python. preprocess() then runs server-side: plain values pass through, type="index" swaps the value for its position, and unknown values raise gradio.exceptions.Error before your function ever sees them. The reverse path is postprocess(), which hands strings and numbers straight back to the frontend — Radio does no serialization of its own, which is why numeric choices and tuple choices work without special-casing.