All guides

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 isA Python library that builds a web UI and an API around your functions
Installpip install --upgrade gradio (Python 3.10 or newer)
Runpython app.py, then open http://localhost:7860
Hot reloadgradio app.py reloads the app every time you save
Simple appsgr.Interface: one function, inputs and outputs
Custom layoutsgr.Blocks: rows, columns, tabs and your own event wiring
Chatbotsgr.ChatInterface, with streaming through yield
API accessEvery app has an HTTP API and a Python client (gradio_client)
AI agentsdemo.launch(mcp_server=True) turns your functions into MCP tools
HostingHugging Face Spaces (gradio deploy), Docker, or mounted inside FastAPI
LicenseApache-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:

  1. Components are the inputs and outputs: Textbox, Image, Audio, File, Dataframe, Chatbot, Slider and dozens more. Each one converts between what the browser sends and a Python value. gr.Image(type="pil") hands your function a PIL image, while type="filepath" hands it a path.
  2. Events connect components to functions. A button's .click, a text box's .submit, or a slider's .release runs a function with some components as inputs and writes the result to others.
  3. 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:

OptionWhat it does
examplesClickable example inputs shown under the form
cache_examples=TrueRuns the examples once at startup so they load instantly
live=TrueRe-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, articleText above and below the app, in Markdown
api_nameThe 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.Row places children side by side and gr.Column stacks them. scale sets relative width, so the left column is twice as wide. gr.Tab, gr.Accordion, gr.Group and gr.Sidebar work the same way.
  • Multiple outputs. analyze returns three values, and Gradio assigns them to total, unique and table in 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.ClearButton resets 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 role and content keys, in the same style as LLM APIs.
  • content is a list of blocks, such as [{"type": "text", "text": "Hello"}], not a plain string. Files appear as {"type": "file", "file": {"path": "..."}}. The to_claude_messages helper 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

OptionWhat 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=TrueLets users attach files. The message becomes a dict with text and files keys
save_history=TrueKeeps 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_limitHow 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:

KindHowLifetimeUse it for
GlobalA variable at module levelShared by every user until the server restartsA loaded model, a cache, a database connection pool
Sessiongr.State(initial_value)One browser tab. Cleared on refreshA shopping cart, a multi-step form, the user's uploaded files
Browsergr.BrowserState(initial_value)Saved in the browser's localStorage. Survives refreshesUser 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:

ComponentCommon type valuesYour 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_size in launch() caps uploads, for example "10mb".
  • allowed_paths lists extra absolute paths or folders Gradio may serve, such as a folder of reference images. blocked_paths lists 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/passwd in 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_limit to 10 or more, or set default_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_id for every event that uses the GPU, so they share slots instead of each getting their own.
  • Write async def functions when you call async libraries. They don't take a thread from the pool.
  • Batch GPU work. batch=True, max_batch_size=8 on 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_size so 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 /predict and /chat. Pass api_name="predict" to keep an old name.
  • handle_file uploads 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:

ValueEffect
"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, float and bool. 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=True links as public
  • Set max_file_size, and use blocked_paths for 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 5Gradio 6
gr.Blocks(theme=..., css=...)demo.launch(theme=..., css=...)
gr.Chatbot(type="tuples") or list-of-lists historyMessages only: dicts with role and content. The type argument is gone
History content as a stringcontent is a list of blocks, such as [{"type": "text", "text": "..."}]
show_copy_button=True, show_download_button=True, show_share_button=Truebuttons=["copy"], buttons=["download", "share"] and so on
show_api=False on an eventapi_visibility="undocumented"
api_name=Falseapi_visibility="private"
demo.launch(show_api=False)demo.launch(footer_links=["gradio", "settings"])
Default endpoints /predict and /chatNamed 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) tuplegr.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

ProblemLikely cause and fix
The page loads but nothing happens on clickYour 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 timeThe 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 containerGradio 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 useAnother Gradio app is running. Stop it, or use server_port=7861
Streaming works locally but not in productionA proxy is buffering. Set proxy_buffering off in Nginx, or the equivalent in your load balancer
Broken styles or links behind a proxySet root_path to the public sub-path
TypeError about theme, type or show_copy_buttonCode written for Gradio 5. See the upgrade table above
Chatbot crashes reading historyGradio 6 history content is a list of blocks, not a string. Flatten it as in the chatbot example
Everyone sees the same dataPer-user data is in a global variable. Move it to gr.State
The upload is rejectedIt's bigger than max_file_size, or the file type isn't in the component's file_types
The share link stopped workingShare 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.