gradio · difficulty ◆◆
gr.UploadButton — the file picker that hides inside a button
One button, three events — and only one of them waits until there is actually a file.
There is a Gradio component that will cheerfully run your handler before the user has uploaded anything. It is the one that looks like a button.
$ gr.UploadButton()What it does
gr.UploadButton renders a single button. Clicking it opens the OS file picker; the file that comes back reaches your function as a temp path string (type='filepath', the default) or as raw bytes (type='binary'). file_count='single' gives one value, 'multiple' and 'directory' give a list. There is no drop zone, no chip list, no preview — it is a 40-pixel control that slots into a toolbar next to your other fields.
Why it matters
A gr.File drop zone eats a whole row and dominates a form that is mostly text. UploadButton keeps the baseline grid intact: 'Attach receipt', 'Add exports', 'Upload your filled template' all read as next actions rather than as empty real estate. It is also the only core component that works as input and as output on the same instance — a click can hand the user a generated file, an upload can receive one back.
Example
$ import gradio as gr
import os
def intake_receipt(receipt):
if receipt is None:
return "No receipt uploaded yet."
name = os.path.basename(receipt)
size = os.path.getsize(receipt)
return f"Queued {name} ({size} bytes) for expense claim processing."
with gr.Blocks(title="Expense intake") as demo:
gr.Markdown("### Expense claim")
rec = gr.UploadButton("Attach receipt photo", file_types=["image"],
type="filepath", variant="primary")
out = gr.Textbox(label="status")
rec.upload(intake_receipt, rec, out)
demo.launch()* Running on local URL: http://127.0.0.1:7931
Renders a gr.Blocks page: a Markdown heading '### Expense claim', then one UploadButton labelled 'Attach receipt photo' (variant='primary', so a filled blue button, native picker filtered to image/*), then a Textbox labelled 'status'. Config tree: id=1 markdown, id=2 uploadbutton, id=3 textbox; one dependency with targets=[[2,'upload']], inputs=[2], outputs=[3], api_name='intake_receipt'.
Handler received a NamedString path inside the server's file cache (/home/kmail/.hermes/cache/scratch/gradio/<hash>/receipt.png, 155 bytes for a tiny test PNG) and the app printed 'Queued receipt.png (155 bytes) for expense claim processing.' Driving the same endpoint with no file at all took the guard branch: 'No receipt uploaded yet.'
Tested with gradio 6.18.0.os.path.basename() and os.path.getsize() work directly because the handler argument IS the path string. Guard for None anyway — an unguarded os.path.basename(None) is the most common crash in upload handlers.
$ import gradio as gr
import pandas as pd
import os
def summarise(files):
if not files:
return "no files"
rows, total = [], 0
for f in (files if isinstance(files, list) else [files]):
df = pd.read_csv(f)
rows.append(f"{os.path.basename(f)}: {len(df)} rows x {len(df.columns)} cols")
total += len(df)
return f"{len(rows)} tables, {total} rows total\n" + "\n".join(rows)
with gr.Blocks(title="Bulk CSV intake") as demo:
gr.Markdown("### Monthly batch upload")
with gr.Row():
ub = gr.UploadButton("Add CSV exports", file_count="multiple",
file_types=[".csv", ".tsv"])
ubd = gr.UploadButton("Add a whole folder", file_count="directory")
out = gr.Textbox(label="batch summary", lines=6)
ub.upload(summarise, ub, out)
ubd.upload(summarise, ubd, out)
demo.launch()* Running on local URL: http://127.0.0.1:7932
/…/gradio/components/upload_button.py:93: UserWarning: The `file_types` parameter is ignored when `file_count` is 'directory'.
Renders a Row holding two UploadButtons side by side — 'Add CSV exports' (multi-file picker) and 'Add a whole folder' (directory picker) — above a 6-line Textbox labelled 'batch summary'. The two .upload listeners appear as two named endpoints, /summarise and /summarise_1.
Feeding two real CSVs (a.csv 3 data rows, b.csv 2 data rows) to either endpoint returned: '2 tables, 5 rows total'. The directory endpoint receives a LIST too, and via the API a bare single file is rejected before your code runs: pydantic ValidationError 'Input should be a valid list' for ListFiles.
Tested with gradio 6.18.0.file_count='directory' returns list[str] exactly like 'multiple' — the only difference is which picker the browser opens. It warns at construction that file_types is ignored, so a folder button accepts everything in the folder.
$ import csv, os, tempfile
import gradio as gr
def make_template(n_rows):
path = os.path.join(tempfile.mkdtemp(), "claim_template.csv")
with open(path, "w", newline="") as fh:
w = csv.writer(fh)
w.writerow(["date", "vendor", "amount", "currency"])
for _ in range(int(n_rows)):
w.writerow(["", "", "", ""])
return path
def check(f):
if f is None:
return "clicked the button but no file was chosen yet"
return f"validating {os.path.basename(f)}"
with gr.Blocks(title="Template + validate") as demo:
gr.Markdown("### Step 1: grab the template. Step 2: upload the filled one.")
rows = gr.Slider(1, 10, value=3, step=1, label="blank rows")
make = gr.UploadButton("Download claim template", variant="primary")
make.click(make_template, rows, make)
filled = gr.UploadButton("Upload your filled template", file_types=[".csv"])
out = gr.Textbox(label="validation")
filled.upload(check, filled, out)
demo.launch()* Running on local URL: http://127.0.0.1:7933
Renders: a Markdown step list, a Slider 'blank rows' (1–10, default 3), an UploadButton 'Download claim template', a second UploadButton 'Upload your filled template', and a Textbox 'validation'. Two dependencies: targets=[[4,'click']] with inputs=[slider] outputs=[uploadbutton], and targets=[[5,'upload']] with inputs=[5] outputs=[textbox].
Calling /make_template with n_rows=5 made the component serve a real generated file: /home/kmail/.hermes/cache/scratch/gradio/<hash>/claim_template.csv, returned to the caller as FileData with orig_name='claim_template.csv'. Calling /check with a real CSV returned 'validating a.csv'.
Tested with gradio 6.18.0.Same instance is input and output: postprocess() wraps your path in FileData and the button turns into a download. Return a path string (or list of them) — postprocess runs Path(value).stat().st_size, so the file has to exist on disk when you return it.
$ # The half-working filter: file_types is only enforced for type='filepath'.
import os
import gradio as gr
def size_of(f):
if isinstance(f, bytes):
return f"GOT {len(f)} RAW BYTES (no filename, no extension check ran)"
return f"GOT PATH {os.path.basename(f)}"
with gr.Blocks() as demo:
a = gr.UploadButton("filepath, image only", type="filepath", file_types=["image"])
b = gr.UploadButton("binary, image only", type="binary", file_types=["image"])
o1, o2 = gr.Textbox(), gr.Textbox()
a.upload(size_of, a, o1)
b.upload(size_of, b, o2)
demo.launch()* Running on local URL: http://127.0.0.1:7934
Two UploadButtons stacked, both labelled 'image only', one filepath one binary, each feeding its own Textbox.
Same 31-byte a.csv pushed at both endpoints:
filepath button -> gr.Error: "Invalid file type. Please upload a file that is one of these formats: ['image']" — rejected before the handler.
binary button -> GOT 31 RAW BYTES — the CSV sailed straight through into the function.
The same PNG through both: 'GOT PATH receipt.png' and 'GOT 155 RAW BYTES'.
Tested with gradio 6.18.0.Reading the installed source confirms why: the extension check sits inside the if self.type == 'filepath' branch of _process_single_file(). The 'binary' branch just does open(path,'rb').read(). Keep type='filepath' if you rely on file_types as a server-side guard.
Common flags
- type='filepath' | 'binary'
- a temp path string vs raw bytes. Renamed in 4.0.0 — the old type='file' now raises ValueError.
- file_count='single' | 'multiple' | 'directory'
- one value or a list; 'directory' picks a folder and returns every file in it.
- file_types=['image']
- must be a list (a bare string raises ValueError). Enforced server-side only in filepath mode, and ignored entirely in directory mode.
- label='Attach receipt photo'
- the button text. Default is 'Upload a File' — always override it, or every button in your app reads the same.
- variant='primary' | 'secondary' | 'stop', size='sm'|'md'|'lg'
- identical to gr.Button; added to UploadButton in 3.34.0. Defaults: secondary, lg.
- .upload(fn, ...) vs .click(fn, ...)
- upload fires only after a file is chosen; click fires on every press and hands you the current value, which is None before the first upload.
- value='template.csv'
- presets the component; gradio copies the file into its cache at startup so the browser can fetch it immediately.
History
It arrived in 3.11.0, in the same release as the Uploadable mixin
Untarring the real PyPI sdists: gradio 3.10.0 (2022-11-17) has no UploadButton anywhere in its Python or Svelte sources — File gained the Uploadable mixin in that release instead. The class first appears in gradio 3.11.0, published 2022-11-23, inheriting Clickable, Uploadable, IOComponent, SimpleSerializable in the then-monolithic gradio/components.py. Ten releases later, 3.35.0 (2023-06-15) split every core component into its own module and gave us gradio/components/upload_button.py.
Two renames and a typo
The constructor signature has drifted three times in ways that bite copy-pasted tutorials: variant and interactive landed in 3.34.0 (PR #4436, merged 2023-06-06), and type's default value was renamed from 'file' to 'filepath' in 4.0.0 (2023-10-31) — in 6.x, passing type='file' dies immediately with ValueError: Invalid value for parameter `type`: file. icon= arrived in 4.8.0 (2023-12-05). Today the constructor takes 19 parameters.
Fun facts
Pros & cons
pros
- + Fits a form. One line tall, aligns with Textboxes and Sliders, no wasted drop-zone real estate.
- + Doubles as a download button. The same instance can be an output — click produces a file the user saves.
- + Three lean events (change, click, upload) make the wiring obvious, unlike gr.File's six.
cons
- − No drag-and-drop and no preview chip — users cannot see what they picked inside the component.
- − file_types is enforced only in filepath mode, so the filter is easy to mistake for validation.
- − file_count='directory' returns a flat list of paths; the folder structure the user picked is not preserved in a usable way.
Takeaways
- 1Guard every upload handler for None — .click() fires before a file exists, and the first press hands you a NoneType.
- 2Keep type='filepath' when you need file_types to actually reject files; type='binary' skips the extension check.
- 3file_count='directory' returns list[str] just like 'multiple' — the only difference is which OS picker opens, and file_types is ignored.
- 4Set label= on every instance; the default is the same string on all of them.
- 5To hand a file back, wire .click() to a function that returns a path and point the output at the button itself.