Interface gives you an input→output pipeline. Blocks gives you the whole web app.
gr.Interface is one function in, one result out. The day that stops being enough, you open the Blocks box — and never really close it again.
gr.Blocks() is Gradio's layout-and-events canvas. You open it as a context manager, place components (Textbox, Slider, Chatbot, …) inside Row/Column/Tab containers in whatever arrangement you want, and wire them to Python functions with event listeners like .click() and .change(). Data flow is fully explicit: every listener declares its inputs and outputs by component reference, and one function's output can be the next function's input.
Interface hard-codes one shape: inputs on the left, outputs on the right, a Submit button in the middle. The moment your app needs tabs, a sidebar, a two-step pipeline (upload → preprocess → generate), or reactive updates that fire on typing, you have left Interface's territory. Blocks is also the substrate everything else runs on — gr.Interface literally subclasses gr.Blocks, gr.ChatInterface is a Blocks template, and gr.mount_gradio_app() expects a Blocks app. Learning it once pays off in every advanced Gradio feature.
import gradio as gr
with gr.Blocks(title="Sentiment Lab") as demo:
gr.Markdown("## Sentiment Lab\nType a sentence, get a verdict.")
with gr.Row():
with gr.Column():
inp = gr.Textbox(label="Sentence", placeholder="e.g. The service was excellent")
btn = gr.Button("Analyze", variant="primary")
with gr.Column():
label = gr.Label(label="Verdict")
conf = gr.Number(label="Confidence %")
btn.click(lambda s: ("POSITIVE 0.94", 94.0), inputs=inp, outputs=[label, conf])
demo.launch(prevent_thread_lock=True)
demo.close()Console: * Running on local URL: http://127.0.0.1:7860 * To create a public link, set `share=True` in `launch()`. Closing server running on port: 7860 Rendered UI (verified on gradio 6.27.0): - a title "Sentiment Lab" with a Markdown intro line - one Row containing two Columns: left column stacks a Textbox labeled "Sentence" (with placeholder) above a primary-styled Button "Analyze"; right column stacks a Label "Verdict" above a Number "Confidence %" - clicking Analyze fills Verdict with "POSITIVE 0.94" and Confidence % with 94 (function verified by direct call)
The classic Interface shape, rebuilt in Blocks. Everything Interface would have positioned for you, you now position yourself — that Row/Column nesting is the entire layout language.
import gradio as gr
with gr.Blocks() as demo:
with gr.Tab("Convert"):
t_in = gr.Textbox(label="Text")
t_out = gr.Textbox(label="UPPERCASED")
t_btn = gr.Button("Upper")
t_btn.click(lambda s: s.upper(), inputs=t_in, outputs=t_out)
with gr.Tab("Count"):
c_in = gr.Textbox(label="Text")
c_out = gr.Number(label="Characters")
c_btn = gr.Button("Count")
c_btn.click(lambda s: len(s), inputs=c_in, outputs=c_out)
demo.launch(prevent_thread_lock=True)
demo.close()Console: * Running on local URL: http://127.0.0.1:7860 * To create a public link, set `share=True` in `launch()`. Closing server running on port: 7860 Rendered UI (verified on gradio 6.27.0): - two tab buttons "Convert" and "Count" above a panel - Convert tab: Textbox "Text" → Button "Upper" → Textbox "UPPERCASED" - Count tab: Textbox "Text" → Button "Count" → Number "Characters" - each tab keeps its own independent component set and its own event listener
Grouping related demos as tabs is the single most common reason people first reach for Blocks — gr.Interface has no equivalent.
import gradio as gr
with gr.Blocks(title="Color Mixer") as demo:
with gr.Row():
r = gr.Slider(0, 255, value=180, label="Red")
g = gr.Slider(0, 255, value=90, label="Green")
b = gr.Slider(0, 255, value=58, label="Blue")
swatch = gr.ColorPicker(label="Mixed color")
for s in (r, g, b):
s.change(lambda r, g, b: f"#{r:02x}{g:02x}{b:02x}",
inputs=[r, g, b], outputs=swatch)
demo.launch(prevent_thread_lock=True)
demo.close()Console: * Running on local URL: http://127.0.0.1:7860 * To create a public link, set `share=True` in `launch()`. Closing server running on port: 7860 Rendered UI (verified on gradio 6.27.0): - one Row of three Sliders: "Red" (start 180), "Green" (90), "Blue" (58), each 0–255 - below, a ColorPicker labeled "Mixed color" - moving any slider instantly updates the swatch: with defaults 180/90/58 it shows #b45a3a (mix function verified by direct call)
Three listeners, one shared output. Loops like this are the payoff of wiring components by reference — the same reactive pattern drives dashboards and live previews.
import gradio as gr
with gr.Blocks(title="Model Garden", analytics_enabled=False) as demo:
gr.Markdown("## Model Garden")
with gr.Accordion("Advanced options", open=False):
temp = gr.Slider(0.0, 1.0, value=0.7, label="Temperature")
chat = gr.Chatbot(label="Assistant")
msg = gr.Textbox(label="Message")
msg.submit(lambda m, h: (h + [{"role": "user", "content": m},
{"role": "assistant", "content": f"echo: {m}"}],
""),
inputs=[msg, chat], outputs=[chat, msg])
demo.launch(prevent_thread_lock=True)
demo.close()Console:
* Running on local URL: http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
Closing server running on port: 7860
Rendered UI (verified on gradio 6.27.0):
- Markdown heading "Model Garden"
- a collapsed Accordion "Advanced options" hiding a Temperature slider (0.0–1.0, default 0.7)
- a Chatbot labeled "Assistant" with an empty conversation, above a Textbox labeled "Message"
- pressing Enter in Message appends the user turn and an 'echo: …' assistant turn to the chat and clears the input (listener verified: appending [{role: user, content: m}, {role: assistant, content: 'echo: '+m}] to history, returning '' to clear)
A minimal chatbot as plain Blocks — this is roughly the skeleton ChatInterface wraps. Building it once yourself demystifies gr.ChatInterface forever.
| Flag | Meaning |
|---|---|
with gr.Blocks() as demo: | The context manager form is the idiom: components created inside the with-block register into this Blocks instance automatically. |
demo.launch() | Blocks apps launch exactly like Interface. Since Gradio 6, app-wide theme= and css= live on launch(), not on the constructor. |
with gr.Row(): / with gr.Column(): | Layout containers are components too — nest them freely; each child lands inside the currently open container. |
comp.click(fn, inputs, outputs) | Event listeners hang off components, not the Blocks object; fn's arguments and return values map positionally onto inputs/outputs. |
fill_height=True | Constructor flag that stretches the app to the full viewport height — what you want for chat-style interfaces. |
delete_cache=None | Constructor option controlling how often Gradio clears cached example files; set to a tuple like (days, hours) for auto-cleanup. |
demo.queue() | Optional pre-launch step to enable the processing queue with default_concurrency_limit — required for streaming outputs. |
The 3.0 release notes call Blocks the headline feature of 'the biggest update to the library, ever': full control over data flows, layout, multiple tabs, and dynamic property updates. The team even ran a 'Gradio Blocks Party' on HuggingFace Spaces to launch it.
Gradio 3 shipped Blocks with with gr.Blocks() as demo: from day one — registration happens by Python context, not by passing lists. Gradio 6 (2026) later moved constructor cosmetics like theme/css to launch(), but the with-block core has barely changed since 2022.
Blocks keeps a stack of currently-open Blocks contexts. gr.Textbox() asks the top of the stack to register itself and receive an id; that's why a component created outside any with-block can't wire events. When the context exits, Gradio freezes the layout into a config dict (mode, components, dependencies) that the Svelte frontend consumes as /config, and every .click()/.change() became one dependency entry mapping event → function → component ids. You can inspect it yourself with demo.get_config_file() — my tab demo serializes as {"mode": "blocks", "components": 5, "dependencies": 2}.