How-to
Automating Reporting WorkflowsDeep dive

Build a Gradio App That Cleans an Uploaded Excel File

Give colleagues a browser tool for cleaning their own Excel files with Gradio: upload a workbook, pick the sheet from a list filled in on upload, choose cleaning options, see a preview and a summary of what changed, and download the cleaned file, with friendly error messages, upload limits, temporary-file clean-up and tests through the Gradio client.

Many cleaning scripts end up being run by their author on other people's behalf: someone emails a messy export, the script runs, the clean file goes back. Turning the script into a small web app lets people do it themselves, and Gradio is one of the quickest ways to get there — a few components and one function become a working page with file upload, options, a preview and a download, no HTML or JavaScript required. This page wraps a pandas cleaning function in a Gradio app: the user uploads a workbook, picks a sheet from a list filled in as soon as the file arrives, ticks the cleaning options, sees a preview and a summary of what changed, and downloads the result. Problems with the file appear as readable messages rather than tracebacks, uploads are size-limited, temporary files are cleaned up, and the whole app is tested through Gradio's Python client. It belongs to Serving Excel Files from Python Web Apps.

Prerequisites

Bash
pip install gradio pandas openpyxl

The examples were run with Gradio 6. The same structure works in Gradio 4 and 5; only some component options have moved between versions. The sample upload is a supplier export with an info sheet in front of the data, padded text, inconsistent headers, a blank row and duplicates — the usual state of files people want cleaned.

Step 1 — A cleaning function that knows nothing about the web

Keep the cleaning logic in a plain function that takes a DataFrame and options and returns the cleaned DataFrame plus a list of what it did. It can then be tested on its own and reused outside the app:

Python
import pandas as pd

def clean_frame(df, trim=True, drop_blank=True, drop_dupes=True):
    notes = []
    renamed = {c: str(c).strip().lower().replace(" ", "_") for c in df.columns}
    if any(k != v for k, v in renamed.items()):
        df = df.rename(columns=renamed)
        notes.append("normalised column names")
    if trim:
        text = df.select_dtypes(include=["object", "string"]).columns
        changed = sum((df[c].notna() & (df[c].str.strip() != df[c])).sum() for c in text)
        for c in text:
            df[c] = df[c].str.strip()
        notes.append(f"trimmed spaces in {changed} cells")
    if drop_blank:
        before = len(df)
        df = df.dropna(how="all")
        notes.append(f"removed {before - len(df)} blank rows")
    if drop_dupes:
        before = len(df)
        df = df.drop_duplicates()
        notes.append(f"removed {before - len(df)} duplicate rows")
    return df.reset_index(drop=True), notes

raw = pd.read_excel("supplier_export.xlsx", sheet_name="Invoices")
cleaned, notes = clean_frame(raw)
print(len(raw), "->", len(cleaned), "rows;", "; ".join(notes))

Six rows become three. Three cells had stray spaces — the notna() test keeps empty cells from being counted, since a missing value never equals itself. Trimming makes " Acme Ltd" and "Acme Ltd" equal, which turns two near-duplicates into exact ones, and the blank row goes. The order of the steps matters for exactly that reason — trim before deduplicating. Cleaning techniques themselves are covered in depth in Cleaning Excel Data with pandas; the app is only a front end to them.

Step 2 — The app

The interface is a gr.Blocks layout. Two event handlers do the work: one fills the sheet list when a file is uploaded, the other cleans and returns a preview, a summary and a file to download:

Python
import tempfile
from pathlib import Path
import gradio as gr

REQUIRED = {"supplier_name", "invoice_no", "amount"}

def list_sheets(path):
    if not path:
        return gr.Dropdown(choices=[], value=None)
    names = pd.ExcelFile(path).sheet_names
    best = next((n for n in names if n.lower() not in {"info", "notes", "readme"}), names[0])
    return gr.Dropdown(choices=names, value=best)

def run_clean(path, sheet, options):
    if not path:
        raise gr.Error("Upload a workbook first.")
    try:
        df = pd.read_excel(path, sheet_name=sheet)
    except Exception as e:
        raise gr.Error(f"Could not read sheet '{sheet}': {e}")
    df, notes = clean_frame(df, trim="Trim spaces" in options, drop_blank="Remove blank rows" in options,
                            drop_dupes="Remove duplicates" in options)
    missing = REQUIRED - set(df.columns)
    if missing:
        raise gr.Error(f"Sheet '{sheet}' is missing column(s): {', '.join(sorted(missing))}.")
    out = Path(tempfile.mkdtemp()) / f"{Path(path).stem}_clean.xlsx"
    with pd.ExcelWriter(out) as xw:
        df.to_excel(xw, sheet_name="Clean", index=False)
        pd.DataFrame({"change": notes}).to_excel(xw, sheet_name="Changes", index=False)
    summary = f"**{len(df)} rows** written. " + "; ".join(notes) + "."
    return df.head(20), summary, str(out)

with gr.Blocks(title="Excel cleaner", delete_cache=(3600, 3600)) as demo:
    gr.Markdown("## Clean an Excel file\nUpload a workbook, choose the sheet and options, then download the cleaned file.")
    with gr.Row():
        upload = gr.File(label="Workbook (.xlsx)", file_types=[".xlsx"], type="filepath")
        with gr.Column():
            sheet = gr.Dropdown(label="Sheet", choices=[])
            options = gr.CheckboxGroup(["Trim spaces", "Remove blank rows", "Remove duplicates"],
                                       value=["Trim spaces", "Remove blank rows", "Remove duplicates"], label="Cleaning")
            go = gr.Button("Clean", variant="primary")
    summary = gr.Markdown()
    preview = gr.Dataframe(label="Preview (first 20 rows)")
    result = gr.File(label="Cleaned workbook")
    upload.upload(list_sheets, upload, sheet, api_name="sheets")
    go.click(run_clean, [upload, sheet, options], [preview, summary, result], api_name="clean")

type="filepath" hands the function the path of the uploaded file in Gradio's temporary directory. Returning a gr.Dropdown(...) from the upload handler updates the existing dropdown's choices and value, so the list of sheets appears as soon as the file lands; the handler pre-selects the first sheet that is not obviously an info or notes sheet. gr.Error shows its message to the user in the page, which is the right place for "this sheet has no Amount column" — a raw traceback helps nobody. The download includes a Changes sheet listing what was done, so the person receiving the file can see why it has fewer rows than they sent.

What happens when someone uses the app The user uploads a workbook; the upload event lists its sheets and fills the dropdown. The user picks options and clicks Clean; the clean handler reads the chosen sheet, cleans it, writes a new workbook and returns a preview, a summary and the file for download. Browser Gradio app pandas 1. upload .xlsx 2. ExcelFile.sheet_names 3. dropdown: Info, Invoices 4. Clean (sheet, options) 5. read, clean, write 6. preview, summary, file

Step 3 — Run it and test it through the client

Launch the app and drive it exactly as a browser would, with gradio_client. Each handler with an api_name is callable:

Python
from gradio_client import Client, handle_file

demo.launch(prevent_thread_lock=True, server_port=7861, quiet=True, max_file_size="20mb", show_error=True)
client = Client("http://127.0.0.1:7861/", verbose=False)

print("sheets:", client.predict(handle_file("supplier_export.xlsx"), api_name="/sheets")["value"])
table, text, path = client.predict(handle_file("supplier_export.xlsx"), "Invoices",
                                   ["Trim spaces", "Remove blank rows", "Remove duplicates"], api_name="/clean")
print(text)
print(pd.read_excel(path, sheet_name="Clean").to_string(index=False))
try:
    client.predict(handle_file("supplier_export.xlsx"), "Info", ["Trim spaces"], api_name="/clean")
except Exception as e:
    print(type(e).__name__ + ":", e)
demo.close()

The upload handler offers both sheets and pre-selects Invoices. Cleaning returns the summary, and the downloaded workbook holds the three clean rows with tidy headers. Choosing the Info sheet produces an AppError carrying the same friendly message the browser would show. Tests like these — written with pytest and a fixture that launches the app — check the whole round trip, including file handling, which testing clean_frame alone does not.

The client is also how other programs can use the tool. Because every event with an api_name is exposed as an endpoint, a scheduled job, a notebook or another team's script can send a workbook to the same app and get the cleaned file back, with exactly the same rules the people using the page get. client.view_api() prints the available endpoints and their parameters, and Gradio adds a "Use via API" link to the page footer that shows the same information to anyone curious. If the app should not be callable this way, set api_name=False on an event, or turn off the API page altogether when launching.

The Invoices sheet before and after cleaning Six rows with padded names, inconsistent headers, a blank row and duplicates become three rows with normalised headers and trimmed text after trimming, removing the blank row and removing duplicates. A B 1 Supplier Name Invoice No 2 Acme Ltd INV-1 3 Brightside & Co INV-2 4 Brightside & Co INV-2 5 6 Acme Ltd INV-1 A B 1 supplier_name invoice_no 2 Acme Ltd INV-1 3 Brightside & Co INV-2 4 Corex GmbH INV-3 before after clean

Step 4 — Before other people use it

A tool on a laptop and a tool used by a team differ in a few settings. max_file_size="20mb" in launch() rejects oversized uploads before your code sees them. delete_cache=(3600, 3600) on the Blocks makes Gradio delete uploaded and generated files older than an hour, checking every hour — uploaded spreadsheets often contain data that should not sit on a server indefinitely. server_name="0.0.0.0" makes the app reachable from other machines, and auth= adds a simple login; for anything beyond a small team, put it behind your organisation's proxy and single sign-on instead. Long-running cleaning jobs are queued by Gradio automatically; demo.queue(default_concurrency_limit=2) limits how many run at once so a few large files cannot exhaust memory. Validating the file more strictly before cleaning — size, sheet structure, macro-enabled formats — is covered in Validate an Uploaded Excel File in a Web App.

Settings worth changing before sharing the app max_file_size rejects large uploads, delete_cache removes old temporary files, server_name and auth control who can reach the app, and the queue's concurrency limit stops several large files exhausting memory. setting protects against upload size max_file_size="20mb" huge uploads temp files delete_cache=(3600, 3600) data left on disk access server_name, auth anyone on the network load queue concurrency memory exhaustion

Common pitfalls

  • Cleaning logic inside the handler. Keep it in a plain function you can test and reuse.
  • Tracebacks shown to users. Raise gr.Error with a readable message.
  • Deduplicating before trimming. Near-duplicates survive; trim first.
  • Uploaded files kept forever. Set delete_cache or clean up yourself.
  • No limits. Set max_file_size and a concurrency limit before sharing.

Conclusion

To let people clean their own Excel files, put the cleaning in a plain pandas function and wrap it in a Gradio Blocks app: a file upload whose upload event fills a sheet dropdown, options as a checkbox group, and a button whose handler returns a preview, a summary and a cleaned workbook with a sheet listing the changes. Use gr.Error for problems, limit upload size and concurrency, let delete_cache remove old files, and test the whole round trip with gradio_client.

Frequently asked questions

How do I upload an Excel file in Gradio? Use gr.File with file_types set to .xlsx and type set to filepath; the handler receives the path of the uploaded file and can read it with pandas.

How do I let users download the cleaned file? Write the result to a temporary path and return that path to a gr.File output component; Gradio serves it for download.

How do I show an error message in a Gradio app? Raise gr.Error with the message; Gradio displays it in the page instead of a traceback.

How do I test a Gradio app from Python? Launch it with prevent_thread_lock, then call its handlers through gradio_client's Client.predict with the api_name of each event.

More on excel in web apps

More on excel in web apps →