gradio · difficulty ◆◆
gr.Files — upload a whole batch, in one preset line
gr.Files is not a component — it is gr.File wearing a preset. Which is fine, because that preset is the one you actually want.
Two of Gradio's six file events fire on the same upload, milliseconds apart, with byte-identical payloads. Pick the wrong one and your batch job runs twice.
$ gr.Files()What it does
gr.Files renders one drop zone that accepts many files and hands your function a list of temp path strings. It is literally a subclass of gr.File with file_count pre-set to 'multiple' — the whole source of gr.Files is a docstring and one super().__init__ call. As input you get list[str]; as output, hand it paths and the same class renders download chips.
Why it matters
Batch is the normal case for ML demos: merge these CSVs, transcribe these calls, embed this folder of PDFs. The moment you accept a list, four new questions appear — which file arrived, which one was removed, what order did the user put them in, and which listener fires when. gr.Files answers all four, but only two of those answers are printed in the docs.
Example
$ import csv, gradio as gr
def merge(files):
rows, header = 0, None
for p in files or []:
with open(p, newline="", encoding="utf-8") as f:
r = csv.reader(f)
header = header or next(r)
rows += len(list(r))
return {"files": len(files or []), "data_rows": rows, "columns": header}
with gr.Blocks(title="batch-merge") as demo:
gr.Markdown("# Batch CSV merger")
f = gr.Files(label="Drop your CSVs", file_types=[".csv"], elem_id="f")
b = gr.Button("Merge")
o = gr.JSON(label="merged")
b.click(merge, f, o)
demo.launch()* Running on local URL: http://127.0.0.1:7860
Renders a Blocks page: a Markdown H1, then a gr.Files drop zone labeled 'Drop your CSVs', a Button 'Merge', and a JSON output labeled 'merged'. The drop zone's inner text before upload is exactly 'Drop File Here - or - Click to Upload'.
Uploaded north.csv (32 bytes) and south.csv (23 bytes) from a real Chromium (Playwright). The component rendered two rows: 'north .csv 32.0 B download-icon remove-x' and 'south .csv 23.0 B download-icon remove-x'.
Your function receives the list ['/tmp/gradio/<hash>/north.csv', '/tmp/gradio/<hash>/south.csv'] — both under the SAME hash directory. Clicking Merge returned: { "files" : 2 , "data_rows" : 4 , "columns" : [ "region" , "sales" ] }.
Tested with gradio 6.18.0 / Python 3.13 on 2026-10-09.Both files land inside one hash directory (the per-session cache), not next to each other by chance. Basename collisions do not help you distinguish them — open by full path, never by name.
$ import gradio as gr
with gr.Blocks() as demo:
f = gr.File(type="binary", label="Any file", elem_id="f")
out = gr.Textbox(label="header check")
f.upload(
lambda data: f"bytes={len(data)} head={data[:8]!r}" if data else "no file",
f, out,
)
demo.launch()* Running on local URL: http://127.0.0.1:7860
Renders a Blocks page with one single-file drop zone ('Any file') wired on the file-specific .upload() listener to a 'header check' Textbox.
Uploaded an 11-byte fake GIF (GIF89a + 0xff + five zero bytes). The input element had accept=None, i.e. no filter was applied to the file picker at all. The Textbox printed: bytes=11 head=b'GIF89a\xff\x00'.
type='binary' handed the function real bytes (type bytes, len 11) with no disk round-trip in your code; type='filepath' would have handed the same file as a str path instead.
Tested with gradio 6.18.0 / Python 3.13 on 2026-10-09.Use .upload() here, not .click() on a Button — with binary payloads you want the check to run the instant the file lands, not after a second user action.
$ import gradio as gr, os
def pick(folder):
if not folder:
return "no folder selected"
names = sorted(os.path.basename(p) for p in folder)
exts = sorted({os.path.splitext(n)[1] for n in names})
return f"{len(names)} files: {names[:3]} ... extensions: {exts}"
with gr.Blocks() as demo:
d = gr.File(file_count="directory", label="Pick a folder", elem_id="f")
out = gr.Textbox(label="scan")
gr.Button("Scan").click(pick, d, out)
demo.launch()* Running on local URL: http://127.0.0.1:7860
Renders the same layout as example 2 but the drop zone is folder mode. Playwright confirmed the underlying input carries BOTH webkitdirectory and directory attributes — that is what opens the native folder picker.
Pointed it at a test directory containing a.txt, b.csv, c.png and a nested sub/ directory holding d.txt. The UI showed four rows (c.png, b.csv, a.txt, d.txt). The Textbox printed: "4 files: ['a.txt', 'b.csv', 'c.png'] ... extensions: ['.csv', '.png', '.txt']".
The folder was walked RECURSIVELY, but every path came back flattened into one directory: the nested sub/d.txt arrived as /tmp/gradio/<hash>/d.txt with no 'sub/' component. Tested with gradio 6.18.0 / Python 3.13 on 2026-10-09.Directory mode is recursive and lossy in one specific way: the tree structure is discarded. If a nested file collides on basename with a top-level one, you cannot tell them apart from the paths alone.
Common flags
- gr.Files()
- template preset for gr.File(file_count='multiple') — imported from gradio.templates, not a separate component class.
- file_count='multiple' | 'single' | 'directory'
- gr.Files defaults to 'multiple'; passing file_count to gr.Files overrides the preset and it really does change (verified: 'single', 'multiple' and 'directory' all took effect on the instance).
- type='filepath' | 'binary'
- 'filepath' (default) gives list[str] temp paths, each one a NamedString — a str subclass that also exposes .name holding the full path.
- file_types=['.csv']
- frontend filter AND a server-side guard: a non-matching upload raises gr.Error('Invalid file type...') before your function runs.
- .upload()
- fires once per upload action, after the file lands. Payload is the FULL accumulated list, not just the new file.
- .delete()
- fires when a user clicks the per-row remove button. Add evt: gr.DeletedFileData to get evt.file (a FileData with path, orig_name, size) for the removed file.
- allow_reordering=True
- rows become draggable — the rendered rows get draggable='true' (Playwright counted 2 for 2 files), one per uploaded file.
- buttons=[...]
- 6.18 default resolves to an empty list; supply your own Button objects to swap the built-in per-row controls.
History
It shipped as a template, not a component
gr.Files has been on the shelf since the Gradio 3.0 sdist: gradio/templates.py contained a 13-class block of presets, and class Files at line 90 was five lines long — a class statement, a three-line docstring, and super().__init__(file_count='multiple', **kwargs). Gradio 3.0 (2022-05-16) is the first release where the module exists in that form. In 4.0.0 the preset was given explicit typed parameters and the block was pruned from 13 classes to 7; in 6.18.0 the module holds 10, and Files is still one of them.
The file events arrived one PR at a time
File's upload listener came in Gradio 3.7 (2022-10-27, PR #2448) for File, Image, Audio and Video together. file_types came in 3.16.0 (2023-01-05, PR #2901). The .delete() event is far younger — PR #8417, listed under the CHANGELOG heading for 4.34.0. But there is no 4.34.0 on PyPI at all: the release line skips straight from 4.33.0 (2024-06-05) to 4.35.0 (2024-06-06), so .delete() reached real installs in 4.35.0, one day later. And height on File was broken for years in between — PR #10209, in 5.10.0 (2025-01-07), is titled 'Ensure the height param in gr.File works as expected'.
Fun facts
Pros & cons
pros
- + Batch upload with one line — gr.Files is gr.File(file_count='multiple') without the parameter bookkeeping.
- + Six real listeners, including .delete() which hands you evt.file.path for the exact file the user removed.
- + Dual direction: the same class that ingests a list of paths renders that list back as download chips.
cons
- − Directory mode is recursive but flattens paths — nested structure is gone by the time your function sees it.
- − The upload/change double-fire is undocumented in the component docs and bites anyone with a heavy handler.
- − file_types rejects plausible spellings like 'txt'; you find out at upload time, not at construction.
Takeaways
- 1.upload() and .change() both fire once per upload with the same full-list payload — pick one, or you run the job twice.
- 2file_types wants '.csv' or 'text'; a bare 'txt' raises gr.Error at upload time, and the check runs server-side.
- 3Directory mode walks subfolders recursively but flattens every path — do not rely on folder structure surviving.
- 4Each file's path is a NamedString: it IS a str, so use str(v) or os.path.basename(v); .name also holds the full path.
- 5For per-file removal UX, use .delete() with a gr.DeletedFileData annotation — evt.file.path names exactly what vanished.