gradio · difficulty ◆
gr.ChatInterface — a full chat UI in one line
One function + one line = a chatbot UI. It is Gradio's highest-leverage shortcut.
The UI that powered thousands of HuggingFace chat Spaces takes ONE line of Python to create — but its fn signature trips almost everyone.
AI-generated draft · reviewed and editedThis text was generated with AI as a first draft, then reviewed and edited by the author, who holds editorial responsibility for it.$ gr.ChatInterface()- 3.37.0
- Introduced
- 34
- Constructor params
- 15
- Components wired
released 2023-07-17 (verified via PyPI upload timestamp)
in 6.28.0, from the live signature — up from 14 in 3.37.0
Blocks components built by the default constructor in 6.28.0 (18 with examples + additional_inputs)
What it does
gr.ChatInterface(fn=...) builds a complete chat web app around your function: a Chatbot panel, a message Textbox, Submit and Stop buttons, a Clear button, autoscroll, and a queue — all wired together. Your fn receives (message, history) and returns the assistant reply as a string. History is a list of message dicts ({'role': ..., 'content': ...}). Set multimodal=True and the input becomes a MultimodalTextbox so users can attach files; the message arrives as a dict {'text': ..., 'files': [...]}.
Why it matters
Chat is the interface people expect from LLM products now. ChatInterface gives you the polished default — streaming replies, retry, undo, copy buttons, auto-generated API endpoint — without writing a single event listener. When you outgrow it (custom message rendering, side panels, login), drop down to gr.Blocks and keep everything else.
Parameters you will reach for next
Beyond fn, multimodal, chatbot, textbox, additional_inputs and examples, the 6.28.0 constructor has a few worth knowing. additional_outputs=[gr.Image()] lets your fn return extra values that land in components you defined in the same Blocks scope. save_history=True (default False) keeps conversations in the browser's local storage and lists them in a side panel. autoscroll defaults to True, autofocus and show_progress are cheap to flip, and concurrency_limit caps how many chats run at once. api_name, api_description and api_visibility rename or hide the auto-generated endpoint. editable is False by default; set it True and users can fix a message before resending.
Example
$ import gradio as gr
def echo(message, history):
return f"You said: {message}"
demo = gr.ChatInterface(fn=echo, title="Echo Bot")
demo.launch()
# fn signature: (message: str, history: list) -> str
# gr.ChatInterface.__init__ has 34 parameters in Gradio 6.28.0Console:
* Running on local URL: http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
Rendered UI (verified by introspecting demo.blocks — 15 components, 5 of them hidden state holders: one BrowserState plus four State objects):
Column
├─ Markdown (title "Echo Bot")
├─ Chatbot (the message thread, empty at start)
└─ Group
├─ Row
│ └─ Textbox ("Type a message...", round pill, Enter-to-send)
│ └─ Form (submit + stop buttons rendered together)
└─ JSON (hidden internal state holder)
Typing "hi" and hitting Enter appends your message bubble on the right,
then "You said: hi" appears in an assistant bubble on the left.
Bonus — call it without a browser via the REST API it auto-exposes:
POST /gradio_api/call/echo {"data": ["hi", []]}
→ event: complete\ndata: ["You said: hi", null]The fn gets (message, history) and returns ONE string — that's the whole contract.
$ import gradio as gr
def streamy(message, history):
for word in ["alpha ", "beta ", "gamma"]:
yield word
demo = gr.ChatInterface(fn=streamy)
demo.launch()
# Same fn, streaming: yield instead of returnConsole:
* Running on local URL: http://127.0.0.1:7860
Rendered UI: the same tree minus the title Markdown — ChatInterface only adds that
component when you pass title=, so this app has 14 components:
Column > Chatbot > Group > Row > Textbox + Form/Button > JSON, plus the five
hidden state components.
The reply bubble types out word by word: "alpha ", then "beta ", then "gamma".
Gradio auto-detected the generator function and switched to streaming mode —
no queue() call needed.
Same call over the REST API shows the progressive stream:
POST /gradio_api/call/streamy {"data": ["one two three", []]}
→ event: generating\ndata: ["alpha ", null]
→ event: generating\ndata: ["beta ", null]
→ event: generating\ndata: ["gamma", null]
→ event: complete\ndata: ["gamma", null]Any generator fn streams for free — no extra parameters, no queue config needed.
$ import gradio as gr
def vision_chat(message, history):
files = [f.split("/")[-1] for f in message.get("files", [])]
return f"Got text={message.get('text')!r}, files={files}"
demo = gr.ChatInterface(
fn=vision_chat,
multimodal=True,
title="Vision Helper",
)
demo.launch()
# message is a dict: {"text": str, "files": [filepath, ...]}Console:
* Running on local URL: http://127.0.0.1:7860
Rendered UI (15 components): same skeleton as the basic app, but the input
Row now holds a MultimodalTextbox — a message field with a 📎 attach button.
Drag an image called photo.webp into the box, type "what is this?", send:
your bubble shows the image preview + text; the assistant bubble replies:
"Got text='what is this?', files=['photo.webp']"
multimodal=False (the default) keeps the plain single-line Textbox.multimodal=True changes the message type from str to dict — code both branches or pin the format.
$ import gradio as gr
def greet(message, history, system_prompt):
return f"[{system_prompt}] {message}"
demo = gr.ChatInterface(
fn=greet,
additional_inputs=[
gr.Textbox("You are helpful.", label="System prompt"),
],
examples=[["hello", "You are terse."], ["who are you", "You are HAL."]],
)
demo.launch()
# extra fn args come from additional_inputs, AFTER (message, history)
# with additional_inputs, examples must be lists of listsConsole:
* Running on local URL: http://127.0.0.1:7860
Rendered UI (18 components — the extra inputs add 4):
Column > Markdown > Chatbot > Group > Row > Textbox + Button > JSON
plus:
├─ Accordion ("Additional Inputs", collapsed) > Textbox ("System prompt")
└─ Dataset (the 2 clickable example chips under the input)
Expand the accordion, set the system prompt to "You are terse.",
type "hello" and send: assistant bubble shows "[You are terse.] hello".
Clicking the "who are you" example chip fills the message AND switches
the system prompt value to "You are HAL." before sending.Examples become a Dataset of clickable chips; with additional_inputs each example row must include a value for every extra input.
Common flags
- fn
- Your chat function: (message, history) -> str | generator yielding str chunks.
- multimodal
- True swaps the Textbox for a MultimodalTextbox; message becomes {'text': ..., 'files': [...]} (introduced in 4.22.0).
- additional_inputs
- Extra components passed to fn after (message, history); rendered inside an Accordion under the chat.
- chatbot
- Pass your own gr.Chatbot(...) to control height, avatar images, or placeholder before the first message.
- textbox
- Pass your own gr.Textbox(...) or gr.MultimodalTextbox(...) to customize placeholder, lines, or initial value.
- examples
- Clickable starter prompts rendered as chips; must be a list of lists when additional_inputs are present.
- editable
- False by default; True lets users edit a sent message before resubmitting.
History
Born in 3.37.0 (July 2023)
ChatInterface shipped as an exported class in Gradio 3.37.0 (PyPI upload 2023-07-17). Back then history was a list of two-element [user, bot] lists — the dict {'role': ..., 'content': ...} format only became the norm in the 4.x line, and 6.0 removed the tuples format from gr.Chatbot outright. I verified 3.36.1 has no ChatInterface in gradio/__init__.py and 3.37.0 does, straight from the wheels on PyPI.
From tuples to dicts to built-in
multimodal=True arrived in 4.22.0 (I bisected the wheels: 4.21.0 has no multimodal param, 4.22.0 does). In 5.x the Chatbot switched fully to the messages format; in 6.x ChatInterface dropped its own 'type' parameter and just speaks messages dicts. The pattern is Gradio's whole history in one component: a demo-first convenience that later hardens into the standard.
Under the hood
Under the hood: it IS a Blocks app
ChatInterface subclasses gr.Blocks and constructs its own components in __init__: a Column with a Markdown header, a Chatbot, a Group holding the Textbox and submit/stop Buttons, plus State/BrowserState objects tracking conversation state. It then wires .submit on the Textbox and .click on the buttons to an internal method that appends the user message, calls your fn, and streams the reply back into the Chatbot. That's why len(demo.blocks) == 15 for a bare ChatInterface — and why you can reach demo.chatbot to customize the underlying Chatbot after construction.
Fun facts
Pros & cons
pros
- + Fastest path from function to chat demo — one line, professional result
- + Streaming, retry, edit, stop, clear, and REST API wired for you
- + Escape hatch built in: swap chatbot/textbox or drop to gr.Blocks anytime
cons
- − Fixed layout — custom side panels need a plain gr.Chatbot inside gr.Blocks
- − fn signature changed across major versions; old 3.x/4.x chat code breaks on 6.x
- − No per-message metadata control unless you supply a custom gr.Chatbot
Takeaways
- 1gr.ChatInterface(fn) — one line from function to deployed chat app
- 2fn signature is (message, history) -> str; history is a list of role/content dicts
- 3Yield instead of return and you get token streaming for free
- 4multimodal=True turns the input into an attach-files field (message becomes a dict)
- 5Outgrown it? Same fn works with a hand-built gr.Chatbot + .submit() in gr.Blocks
