How to Set Up and Use Gradio: A Complete Guide to Python Web UIs for AI Apps
Set up Gradio 6 and build AI web apps in Python: Interface and Blocks, streaming LLM chatbots, state, API and MCP access, authentication and deployment.
By Muhammad Saif · Updated
Gradio is an open-source Python library that turns a Python function into a web app. You describe the inputs and outputs, and Gradio builds the page, runs a server, queues requests and gives you an API for free. It's the quickest way to put a model, a RAG pipeline or an internal tool in front of people who don't use a terminal.
This guide covers Gradio from installation to production: the two ways to build an app, streaming chatbots, state, files, performance, using your app as an API or an MCP server for AI agents, authentication and deployment. Every code sample was run against Gradio 6.30.0 (released 8 October 2026) on Python 3.12.
The short version
| What it is | A Python library that builds a web UI and an API around your functions |
| Install | pip install --upgrade gradio (Python 3.10 or newer) |
| Run | python app.py, then open http://localhost:7860 |
| Hot reload | gradio app.py reloads the app every time you save |
| Simple apps | gr.Interface: one function, inputs and outputs |
| Custom layouts | gr.Blocks: rows, columns, tabs and your own event wiring |
| Chatbots | gr.ChatInterface, with streaming through yield |
| API access | Every app has an HTTP API and a Python client (gradio_client) |
| AI agents | demo.launch(mcp_server=True) turns your functions into MCP tools |
| Hosting | Hugging Face Spaces (gradio deploy), Docker, or mounted inside FastAPI |
| License | Apache-2.0 |
When Gradio is the right tool
Gradio fits when the app is a function with a UI: something goes in, your Python code runs, something comes out.
Good fits:
- Demos of a model or AI feature for clients, investors or your own team
- Internal tools such as document Q&A, text classification, report generation or data clean-up
- A test bench for prompts, retrieval settings or model versions, so non-engineers can try them
- A quick front end for an MCP tool or an API you already have
Weaker fits:
- A customer-facing product that needs your own design system. Build a proper front end and keep Gradio for internal use
- Dashboards with many charts and filters that change together. Streamlit or a JavaScript dashboard is usually simpler
- Complex multi-page apps with routing, accounts and billing
A common pattern is to start with Gradio to prove the idea works, then replace the UI once the logic has settled. Because the logic lives in plain Python functions, nothing is wasted.
Install Gradio
Gradio needs Python 3.10 or newer. Check your version first:
python3 --version
Create a virtual environment so Gradio and its dependencies don't clash with other projects:
mkdir gradio-demo && cd gradio-demo
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade gradio
If you use uv, the same setup is:
uv venv && source .venv/bin/activate
uv pip install gradio
Confirm the install:
python -c "import gradio; print(gradio.__version__)"
For anything you'll deploy, pin the version in requirements.txt (for example gradio==6.30.0). Gradio releases often, and pinning means a new release can't change your app without you knowing.
Your first Gradio app
Save this as app.py:
import gradio as gr
def greet(name: str, intensity: int) -> str:
return "Hello, " + name + "!" * int(intensity)
demo = gr.Interface(
fn=greet,
inputs=[gr.Textbox(label="Name"), gr.Slider(1, 10, value=2, step=1, label="Intensity")],
outputs=gr.Textbox(label="Greeting"),
title="Greeter",
examples=[["Ada", 3], ["Linus", 1]],
)
if __name__ == "__main__":
demo.launch()
Run it:
python app.py
Open http://localhost:7860. You'll see a form with a text box and a slider, a submit button, an output box and clickable examples. Gradio called greet with the values from the inputs, in order, and put the return value in the output.
Hot reload while you develop
Restarting the server after every change gets old quickly. Run the file with the gradio command instead of python:
gradio app.py
Gradio now reloads the page every time you save. It looks for a variable named demo; if yours is called something else, pass --demo-name app.
Hot reload re-runs your whole file, which is slow if you load a model at the top. Put expensive setup inside an if gr.NO_RELOAD: block so it runs once:
import gradio as gr
if gr.NO_RELOAD:
from transformers import pipeline
classifier = pipeline("sentiment-analysis") # loaded once, not on every save
Share a temporary public link
To show the app to someone outside your network without deploying it:
demo.launch(share=True)
Gradio prints a https://xxxx.gradio.live URL that tunnels to your machine. The link lasts up to one week and stops working when you stop the script. Anyone with the link can use the app and run your code on your machine, so add authentication (covered below) before you share anything that touches private data.
How Gradio works
Three ideas explain almost everything in Gradio:
- Components are the inputs and outputs:
Textbox,Image,Audio,File,Dataframe,Chatbot,Sliderand dozens more. Each one converts between what the browser sends and a Python value.gr.Image(type="pil")hands your function a PIL image, whiletype="filepath"hands it a path. - Events connect components to functions. A button's
.click, a text box's.submit, or a slider's.releaseruns a function with some components as inputs and writes the result to others. - The queue sits between the browser and your functions. Every event goes through it, so long-running jobs don't time out, users see their place in line, and you control how many jobs run at once.
On top of these, Gradio exposes every event as an API endpoint. That's why the same app works in a browser, from Python, over HTTP and as an MCP server.
gr.Interface: one function, inputs and outputs
gr.Interface is the high-level class. You give it a function, a list of inputs and a list of outputs, and it builds the whole page. The first sample above is an Interface.
Inputs and outputs can be component objects or string shortcuts:
# These two lines are equivalent
gr.Interface(fn=greet, inputs=["text", "slider"], outputs="text")
gr.Interface(fn=greet, inputs=[gr.Textbox(), gr.Slider()], outputs=gr.Textbox())
Use component objects when you need labels, defaults or settings. Some useful Interface options:
| Option | What it does |
|---|---|
examples | Clickable example inputs shown under the form |
cache_examples=True | Runs the examples once at startup so they load instantly |
live=True | Re-runs the function whenever an input changes, with no submit button |
flagging_mode="manual" | Adds a Flag button that saves inputs and outputs to a CSV, useful for collecting bad results |
title, description, article | Text above and below the app, in Markdown |
api_name | The endpoint name. It defaults to the function name, so this app's endpoint is /greet |
Here's a more useful example: an image filter that takes an upload and returns a processed image.
import gradio as gr
from PIL import Image, ImageFilter, ImageOps
def apply_filter(image: Image.Image, effect: str, strength: float) -> Image.Image:
if image is None:
raise gr.Error("Upload an image first.")
if effect == "Grayscale":
return ImageOps.grayscale(image)
if effect == "Blur":
return image.filter(ImageFilter.GaussianBlur(radius=strength))
return image.filter(ImageFilter.UnsharpMask(radius=strength, percent=150))
demo = gr.Interface(
fn=apply_filter,
inputs=[
gr.Image(type="pil", label="Input"),
gr.Radio(["Grayscale", "Blur", "Sharpen"], value="Blur", label="Effect"),
gr.Slider(0.5, 10, value=2, label="Strength"),
],
outputs=gr.Image(label="Output", buttons=["download", "fullscreen"]),
)
if __name__ == "__main__":
demo.launch(max_file_size="10mb")
Two details worth copying:
raise gr.Error(...)shows a friendly error in the UI instead of a stack trace. Use it for anything the user can fix.max_file_size="10mb"rejects large uploads before they reach your code. Set it on every app that accepts files.
gr.Blocks: custom layouts and events
When you need more than one function, a custom layout, or outputs that feed into other steps, use gr.Blocks. You lay out components inside a with block and wire up events yourself.
from collections import Counter
import gradio as gr
def analyze(text: str, min_length: int):
words = [w.strip(".,!?;:\"'()").lower() for w in text.split()]
kept = [w for w in words if len(w) >= min_length]
top = Counter(kept).most_common(10)
return len(words), len(set(kept)), [[word, count] for word, count in top]
with gr.Blocks(title="Text analyzer") as demo:
gr.Markdown("# Text analyzer\nPaste some text and see which words it uses most.")
with gr.Row():
with gr.Column(scale=2):
text = gr.Textbox(label="Text", lines=8, placeholder="Paste text here")
min_length = gr.Slider(1, 10, value=4, step=1, label="Ignore words shorter than")
with gr.Row():
analyze_btn = gr.Button("Analyze", variant="primary")
clear_btn = gr.ClearButton()
with gr.Column(scale=1):
total = gr.Number(label="Total words")
unique = gr.Number(label="Unique words kept")
table = gr.Dataframe(headers=["Word", "Count"], label="Top words")
status = gr.Markdown()
gr.on(
triggers=[analyze_btn.click, min_length.release],
fn=analyze,
inputs=[text, min_length],
outputs=[total, unique, table],
).then(lambda: "Done.", outputs=status)
clear_btn.add([text, total, unique, table, status])
if __name__ == "__main__":
demo.launch()
What's happening:
- Layout.
gr.Rowplaces children side by side andgr.Columnstacks them.scalesets relative width, so the left column is twice as wide.gr.Tab,gr.Accordion,gr.Groupandgr.Sidebarwork the same way. - Multiple outputs.
analyzereturns three values, and Gradio assigns them tototal,uniqueandtablein order. - Several triggers, one handler.
gr.on(triggers=[...])runs the same function when the button is clicked or the slider is released. - Chaining.
.then(...)runs the next step after the first finishes. Use it for multi-step pipelines such as retrieve, then generate, then log..success(...)runs only if the previous step didn't raise an error. - Clear button.
gr.ClearButtonresets whichever components you add to it.
Updating component properties
A function can change a component's settings as well as its value. Return a new instance of the component with the properties you want:
def lock_form(accepted: bool):
return gr.Button(interactive=accepted), gr.Textbox(visible=accepted)
accept.change(lock_form, inputs=accept, outputs=[submit_btn, notes])
This shows, hides, enables or relabels parts of the UI based on what the user does.
Running JavaScript in the browser
For instant UI changes that don't need Python, pass js= to an event. For example, btn.click(None, js="() => window.scrollTo(0, 0)") scrolls to the top without a round trip to the server.
Build a chatbot with gr.ChatInterface
gr.ChatInterface gives you a complete chat UI from one function. The function receives the new message and the conversation history, and returns or yields the reply. Yielding streams the reply token by token, and Gradio turns the send button into a stop button while it streams.
Here's a streaming chatbot backed by Claude through the Anthropic Python SDK (pip install anthropic, then set ANTHROPIC_API_KEY):
import anthropic
import gradio as gr
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
SYSTEM_PROMPT = "You are a concise, friendly assistant for a small engineering team."
def to_claude_messages(history: list[dict]) -> list[dict]:
"""Convert Gradio's chat history into the Claude Messages API format."""
messages = []
for message in history:
content = message["content"]
if isinstance(content, str):
text = content
else: # Gradio 6 stores content as a list of blocks
text = "\n".join(block["text"] for block in content if block.get("type") == "text")
if text:
messages.append({"role": message["role"], "content": text})
return messages
def respond(message: str, history: list[dict]):
messages = to_claude_messages(history) + [{"role": "user", "content": message}]
reply = ""
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=64000,
system=SYSTEM_PROMPT,
messages=messages,
output_config={"effort": "low"}, # chat rarely needs deep reasoning
# If the model declines, retry on Anthropic's recommended fallback model
extra_headers={"anthropic-beta": "server-side-fallback-2026-07-01"},
extra_body={"fallbacks": "default"},
) as stream:
for text in stream.text_stream:
reply += text
yield reply
if stream.get_final_message().stop_reason == "refusal":
yield reply + "\n\n*The model declined to answer this one.*"
demo = gr.ChatInterface(
fn=respond,
title="Team assistant",
examples=["Explain idempotency in one paragraph", "Write a regex for UK postcodes"],
)
if __name__ == "__main__":
demo.launch()
Things to know about chat history in Gradio 6:
- History is a list of messages with
roleandcontentkeys, in the same style as LLM APIs. contentis a list of blocks, such as[{"type": "text", "text": "Hello"}], not a plain string. Files appear as{"type": "file", "file": {"path": "..."}}. Theto_claude_messageshelper above flattens the text blocks so the code works with any version.- Yield the full reply so far on each step, not just the new token. Gradio replaces the message each time.
- The model doesn't remember anything between calls. You send the whole history every time, which is why long chats cost more. Trim or summarise old turns if conversations run long.
Useful ChatInterface options
| Option | What it does |
|---|---|
additional_inputs=[gr.Textbox(...), gr.Slider(...)] | Extra controls (system prompt, temperature, document filter) shown in an accordion and passed to your function after history |
multimodal=True | Lets users attach files. The message becomes a dict with text and files keys |
save_history=True | Keeps past conversations in the browser so users can return to them |
chatbot=gr.Chatbot(height=500) | Customises the chat window: height, avatars, buttons=["copy", "copy_all"] and more |
flagging_mode="manual" | Adds feedback buttons so users can mark bad answers |
concurrency_limit | How many chats can generate at the same time (see the performance section) |
For a chat over your own documents, retrieval goes inside respond: search your index for passages related to message, put them in the prompt with their sources, and stream the answer. The UI code doesn't change.
Managing state
Gradio functions don't remember anything between calls unless you tell them to. There are three kinds of state:
| Kind | How | Lifetime | Use it for |
|---|---|---|---|
| Global | A variable at module level | Shared by every user until the server restarts | A loaded model, a cache, a database connection pool |
| Session | gr.State(initial_value) | One browser tab. Cleared on refresh | A shopping cart, a multi-step form, the user's uploaded files |
| Browser | gr.BrowserState(initial_value) | Saved in the browser's localStorage. Survives refreshes | User preferences, a saved API key the user enters themselves |
Session state is the one you'll use most. Pass it as an input, return its new value as an output:
import gradio as gr
def add_item(item: str, cart: list[str]):
if item.strip():
cart = cart + [item.strip()]
return cart, "\n".join(f"- {i}" for i in cart), ""
with gr.Blocks() as demo:
cart = gr.State([]) # one list per browser tab
item = gr.Textbox(label="Add an item and press Enter")
items = gr.Markdown()
item.submit(add_item, inputs=[item, cart], outputs=[cart, items, item])
if __name__ == "__main__":
demo.launch()
Two rules for gr.State: the value must be something Python can deep-copy, and you should return a new value (cart + [item]) instead of changing the old one in place, so Gradio notices the change. For objects that can't be copied, such as an open connection, keep them in a global dictionary keyed by request.session_hash and clean up in demo.unload(...).
Never keep per-user data in a global variable. Every visitor would see and change the same value.
Files, images and media
Media components hand your function a value in the format you choose with type:
| Component | Common type values | Your function receives |
|---|---|---|
gr.Image | "pil", "numpy", "filepath" | A PIL image, a NumPy array, or a path on disk |
gr.Audio | "numpy", "filepath" | A (sample_rate, array) tuple, or a path |
gr.File | "filepath", "binary" | A path, or the raw bytes |
gr.Video | (always a path) | A path to the uploaded video |
To return a file, return a path to it. Gradio copies uploads and outputs into a temporary cache and serves them from there.
Security settings to know before you deploy:
max_file_sizeinlaunch()caps uploads, for example"10mb".allowed_pathslists extra absolute paths or folders Gradio may serve, such as a folder of reference images.blocked_pathslists ones it must never serve, and wins if the two overlap.- Never return a path built from user input without checking it. A path like
../../etc/passwdin a text box shouldn't become a download.
Performance: the queue and concurrency
Every event goes through Gradio's queue. By default, each event runs one job at a time. That's a safe default for a GPU model that can only handle one request, and a slow one for an app that mostly waits on API calls.
Tune it in three places:
demo.queue(
default_concurrency_limit=5, # jobs per event that may run at once (default 1)
max_size=50, # reject new jobs when 50 are waiting
)
# Per event, overriding the default
generate_btn.click(generate, inputs, outputs, concurrency_limit=2, concurrency_id="gpu")
upscale_btn.click(upscale, inputs, outputs, concurrency_id="gpu") # shares the same 2 slots
demo.launch(max_threads=40) # thread pool for regular def functions (default 40)
How to choose the numbers:
- Calls to an external API (an LLM, a search service): raise
concurrency_limitto 10 or more, or setdefault_concurrency_limit=None. Your server is mostly waiting, and the API's rate limit is the real cap. - A local GPU model: keep the limit at 1 or 2 per GPU, depending on memory. Use the same
concurrency_idfor every event that uses the GPU, so they share slots instead of each getting their own. - Write
async deffunctions when you call async libraries. They don't take a thread from the pool. - Batch GPU work.
batch=True, max_batch_size=8on an event makes Gradio collect up to 8 waiting requests and call your function once with lists, which is often far faster on a GPU. - Set
max_sizeso a traffic spike gets a clear "queue full" error instead of minutes of waiting.
Gradio keeps the queue and session state in the server process's memory. Running several worker processes behind a load balancer gives each one its own queue, and sessions break unless each user is always routed to the same process. Start with one process and raise the concurrency limits, then scale out with sticky sessions if you need to.
Use your Gradio app as an API
Every Gradio app is also an API. Scroll to the footer of a running app and click Use via API to see the endpoints and ready-made code.
From Python, use gradio_client, which is installed with Gradio (or on its own with pip install gradio_client):
from gradio_client import Client, handle_file
client = Client("http://127.0.0.1:7860/") # or a Space name such as "username/space-name"
client.view_api() # prints every endpoint and its parameters
print(client.predict("Ada", 3, api_name="/greet"))
result_path = client.predict(
handle_file("photo.jpg"), "Blur", 2,
api_name="/apply_filter",
)
- Endpoint names default to the function name in Gradio 6 (
/greet,/apply_filter,/respond). Gradio 5 used/predictand/chat. Passapi_name="predict"to keep an old name. handle_fileuploads a local path or URL for file and image inputs.client.submit(...)starts a job without waiting. Call.result()to wait,.status()to check progress, or loop over the job to read streamed outputs.- Private Spaces need a token:
Client("username/private-space", token="hf_..."). - Login-protected apps take
auth=["username", "password"].
There's also a JavaScript client (npm install @gradio/client) and a plain HTTP API you can call with curl.
Controlling what the API exposes
Every event is public by default, including helper events you never meant to expose. Set api_visibility on events and on Interface or ChatInterface:
| Value | Effect |
|---|---|
"public" (default) | Listed on the API page and callable |
"undocumented" | Callable, but hidden from the API page |
"private" | Hidden and not offered to clients. Still reachable over raw HTTP, so it is not an access control |
Use authentication, not api_visibility, to keep people out.
Turn a Gradio app into an MCP server
Gradio can expose your functions as tools for AI agents through the Model Context Protocol (MCP), so assistants like Claude can call them. Install the extra:
pip install "gradio[mcp]"
Then write a function with type hints and a docstring, and launch with mcp_server=True:
import gradio as gr
def letter_counter(word: str, letter: str) -> int:
"""
Count how many times a letter appears in a word or text.
Args:
word: The text to search through.
letter: The single letter to count.
Returns:
The number of occurrences, ignoring case.
"""
return word.lower().count(letter.lower())
demo = gr.Interface(
fn=letter_counter,
inputs=[gr.Textbox(label="Text"), gr.Textbox(label="Letter", max_length=1)],
outputs=gr.Number(label="Count"),
)
if __name__ == "__main__":
demo.launch(mcp_server=True)
Gradio prints the MCP address on startup: http://127.0.0.1:7860/gradio_api/mcp/. Add it to any MCP client that supports Streamable HTTP. The tool schema is at /gradio_api/mcp/schema if you want to check what agents will see.
How Gradio builds each tool:
- The tool name is the function name.
- The description comes from the docstring, and each argument's description from the
Args:section. Agents decide when to call your tool from these, so write them for a reader who has never seen your code. - The input schema comes from the type hints. Keep inputs simple, with
str,int,floatandbool. Strings are the most reliable across MCP clients.
For tools that should exist only for agents, with no UI, use gr.api(fn) inside a gr.Blocks. Gradio also has @gr.mcp.tool(), @gr.mcp.resource(...) and @gr.mcp.prompt() decorators for finer control.
An MCP server runs your code on behalf of whatever agent connects. Put it behind authentication, and only expose functions that are safe for an AI model to call with unexpected input.
Authentication and security
Built-in username and password
For a small internal tool, Gradio's built-in login is enough:
import os
demo.launch(auth=("admin", os.environ["APP_PASSWORD"]))
# Several users
demo.launch(auth=[("alice", os.environ["ALICE_PW"]), ("bob", os.environ["BOB_PW"])])
# Your own check, for example against a database
def check_login(username: str, password: str) -> bool:
return verify_password_hash(username, password)
demo.launch(auth=check_login)
Inside a function, add a request: gr.Request parameter to see who's using the app. Gradio fills it in, and it doesn't appear as an input:
def whoami(request: gr.Request) -> str:
return f"Signed in as {request.username}"
Visiting /logout signs the user out.
Use your existing login with FastAPI
When the app sits inside a product that already has accounts, mount Gradio inside FastAPI and let your own auth decide who gets in:
import gradio as gr
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/health")
def health():
return {"ok": True}
def get_user(request: Request) -> str | None:
# Replace with your real session or token check.
return request.headers.get("x-user")
def whoami(request: gr.Request) -> str:
return f"Signed in as {request.username}"
with gr.Blocks() as demo:
out = gr.Textbox(label="User")
gr.Button("Who am I?").click(whoami, outputs=out)
app = gr.mount_gradio_app(app, demo, path="/tools", auth_dependency=get_user)
Run it with uvicorn main:app. auth_dependency gets every request: return a user ID to let it through, or None to answer with 401 Unauthorized. The returned ID becomes request.username in your Gradio functions. Don't combine it with auth=.
Security checklist
- Keep API keys and passwords in environment variables or your host's secret store, never in
app.py - Put authentication in front of anything that uses private data, costs money per request (LLM calls) or is exposed as an MCP server
- Treat
share=Truelinks as public - Set
max_file_size, and useblocked_pathsfor anything sensitive on the server - Validate inputs in your functions. Agents and API clients don't use your UI's sliders and dropdowns
- Leave
strict_cors=True(the default) unless you know why you need to change it - Turn off usage analytics if your policy requires it, with the environment variable
GRADIO_ANALYTICS_ENABLED=False
Deploy a Gradio app
Hugging Face Spaces
The fastest route. Spaces hosts Gradio apps for free on basic CPU hardware, with paid GPU upgrades. From the folder that has your app.py and requirements.txt:
pip install huggingface_hub
hf auth login
gradio deploy
The command asks for a Space name and hardware, uploads your files (respecting .gitignore) and gives you a URL. Add secrets such as ANTHROPIC_API_KEY in the Space's settings, not in your code. You can also set up a GitHub Action so every push to main redeploys.
Docker on your own server
For a VPS, AWS, GCP, Azure or Kubernetes, package the app in a container:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV GRADIO_SERVER_NAME=0.0.0.0 \
GRADIO_SERVER_PORT=7860 \
GRADIO_ANALYTICS_ENABLED=False
EXPOSE 7860
CMD ["python", "app.py"]
docker build -t gradio-app .
docker run -p 7860:7860 -e ANTHROPIC_API_KEY gradio-app
GRADIO_SERVER_NAME=0.0.0.0 matters. By default Gradio listens only on 127.0.0.1, which is unreachable from outside the container.
Behind Nginx with HTTPS
Put a reverse proxy in front for HTTPS and a proper domain. To serve the app under a sub-path such as https://example.com/gradio/:
location /gradio/ {
proxy_pass http://127.0.0.1:7860/;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
Then launch with the matching path, so Gradio builds its links correctly:
demo.launch(root_path="/gradio")
proxy_buffering off is what makes streaming work. Without it, Nginx holds back streamed chat replies and progress updates until the response finishes.
Inside an existing FastAPI app
The mount_gradio_app example above also works as a deployment: your API and the Gradio UI share one server, one domain and one login. Run it with uvicorn main:app --host 0.0.0.0 --port 8000.
Themes and custom CSS
Gradio ships with themes such as gr.themes.Soft(), gr.themes.Glass(), gr.themes.Monochrome() and gr.themes.Ocean(), and you can adjust colours, fonts and spacing:
theme = gr.themes.Soft(primary_hue="indigo", font=[gr.themes.GoogleFont("Inter"), "sans-serif"])
demo.launch(
theme=theme,
css=".gradio-container { max-width: 960px !important; margin: auto; }",
)
In Gradio 6, theme, css, css_paths, js, head and head_paths go in launch(), not in gr.Blocks(). Give a component elem_id="..." or elem_classes=[...] to target it from your CSS.
Upgrading from Gradio 5 to Gradio 6
Gradio 6 tidied up the Python API, and several common patterns break. Before upgrading, install Gradio 5.50 and run your app. It prints a deprecation warning for each thing that will stop working. The changes most apps hit:
| Gradio 5 | Gradio 6 |
|---|---|
gr.Blocks(theme=..., css=...) | demo.launch(theme=..., css=...) |
gr.Chatbot(type="tuples") or list-of-lists history | Messages only: dicts with role and content. The type argument is gone |
History content as a string | content is a list of blocks, such as [{"type": "text", "text": "..."}] |
show_copy_button=True, show_download_button=True, show_share_button=True | buttons=["copy"], buttons=["download", "share"] and so on |
show_api=False on an event | api_visibility="undocumented" |
api_name=False | api_visibility="private" |
demo.launch(show_api=False) | demo.launch(footer_links=["gradio", "settings"]) |
Default endpoints /predict and /chat | Named after the function. Pass api_name="predict" to keep the old name |
cache_examples="lazy" | cache_examples=True, cache_mode="lazy" |
Client(..., hf_token=...) | Client(..., token=...) |
Video output as a (video, subtitles) tuple | gr.Video(value=video_path, subtitles=subtitle_path) |
Also check gr.Chatbot, which now renders tags such as <thinking> by default (allow_tags=False restores the old behaviour), and gr.HTML, which no longer adds padding by default. The official Gradio 6 migration guide has the full list, including changes to plots, Dataframe, ImageEditor and webcam options.
Troubleshooting common Gradio problems
| Problem | Likely cause and fix |
|---|---|
| The page loads but nothing happens on click | Your function raised an error. Check the terminal, or launch with show_error=True to see errors in the browser |
| Requests wait in line one at a time | The default concurrency limit is 1. Raise concurrency_limit on the event or default_concurrency_limit in queue() |
| Can't reach the app from another machine or a container | Gradio listens on 127.0.0.1. Set server_name="0.0.0.0" or GRADIO_SERVER_NAME=0.0.0.0 |
| Port 7860 is in use | Another Gradio app is running. Stop it, or use server_port=7861 |
| Streaming works locally but not in production | A proxy is buffering. Set proxy_buffering off in Nginx, or the equivalent in your load balancer |
| Broken styles or links behind a proxy | Set root_path to the public sub-path |
TypeError about theme, type or show_copy_button | Code written for Gradio 5. See the upgrade table above |
Chatbot crashes reading history | Gradio 6 history content is a list of blocks, not a string. Flatten it as in the chatbot example |
| Everyone sees the same data | Per-user data is in a global variable. Move it to gr.State |
| The upload is rejected | It's bigger than max_file_size, or the file type isn't in the component's file_types |
| The share link stopped working | Share links last up to a week and stop when the script stops. Deploy for anything permanent |
Frequently asked questions
Is Gradio free?
Yes. Gradio is open source under the Apache-2.0 license, and you can use it in commercial projects. Hugging Face Spaces hosts Gradio apps for free on basic CPU hardware, and charges for GPUs and larger machines.
Gradio or Streamlit?
Gradio is built around functions with inputs and outputs. It gives you a queue, an API, a Python client and an MCP server with no extra work, which suits model demos, chatbots and AI tools. Streamlit re-runs your script from the top on every interaction, which suits data dashboards and exploratory analysis. For an AI feature that other code or agents will also call, Gradio is usually the better choice.
Can I use Gradio in production?
Yes, for internal tools, demos, admin panels and AI test benches. Set concurrency limits, add authentication, cap upload sizes and run it behind HTTPS. For a customer-facing product with its own design, keep your logic in Python and build a dedicated front end that calls it, possibly through Gradio's API.
Does Gradio work with any model or API?
Yes. Gradio doesn't care what your function does. It works with Hugging Face Transformers, PyTorch, scikit-learn, the Claude or OpenAI APIs, a local model through Ollama, or code with no AI at all.
Can I embed a Gradio app in my website?
Yes. Apps hosted on Hugging Face Spaces can be embedded with an iframe or Gradio's web component. A self-hosted app can be embedded with an iframe pointing at its URL. For a deeper integration, call the app's API from your own front end.
How do I keep my API keys safe?
Read them from environment variables (os.environ["ANTHROPIC_API_KEY"]) and set them in your host's secret settings: Space secrets on Hugging Face, -e flags or a secrets manager with Docker. Never commit them, and never send them to the browser.
Next steps
You now have everything you need to build a real Gradio app: a layout in Blocks, a streaming chatbot, per-user state, an API and MCP endpoint, a login and a deployment. A good first project is a small tool your team needs this week, such as a document Q&A over your policies or a form that drafts replies to support emails.
If you'd rather have it built for you, I build custom RAG systems with a chat interface your team can use from day one, in Gradio for internal tools or a custom front end for customers, with source citations, access rules and an evaluation set. Tell me what you need.