To upload a file in Python Tkinter, call filedialog.askopenfilename() from a button, then copy the chosen file to an uploads folder with shutil.copy2() or send it to a server with requests.post(). The dialog only returns a path:
path = filedialog.askopenfilename()
if path:
shutil.copy2(path, "uploads")
My test setup was Python 3.12.5 (Tk 8.6), Pillow 12.3.0 and requests 2.34.2 on Windows 11. The windows below are real; a test hook picks the file so each run is repeatable.
You’ll build a basic upload button, add type and size checks, upload several files into a table, preview an uploaded image, and send a file to a web server.
Build a Tkinter file upload button
This is the complete pattern: a button opens the file dialog, the chosen file is copied into an uploads folder, and a label reports what happened. An empty string means the user cancelled:
import shutil
import tkinter as tk
from pathlib import Path
from tkinter import filedialog
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
def upload_file():
path = filedialog.askopenfilename(title="Choose a file to upload")
if not path: # the user pressed Cancel
status.config(text="No file selected")
return
src = Path(path)
shutil.copy2(src, UPLOAD_DIR / src.name) # copy keeps the original in place
size_kb = src.stat().st_size / 1024
status.config(text=f"Uploaded {src.name} ({size_kb:.1f} KB) to {UPLOAD_DIR}/")
root = tk.Tk()
root.title("File Upload")
root.geometry("420x140")
tk.Button(root, text="Upload File", width=16, command=upload_file).pack(pady=20)
status = tk.Label(root, text="No file uploaded yet")
status.pack()
root.mainloop()

copy2() also keeps the file’s timestamps. Every option the dialog itself takes, like initialdir and save dialogs, is in my Tkinter filedialog guide.
How do I limit the file types and size a Tkinter upload accepts?
The filetypes option only filters what the dialog shows, and users can still switch to “All files”. So check the extension and size again in code before copying:
import shutil
import tkinter as tk
from pathlib import Path
from tkinter import filedialog
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
ALLOWED = {".csv", ".xlsx", ".pdf"}
MAX_MB = 5
def show(message, color):
status.config(text=message, fg=color)
def upload_file():
path = filedialog.askopenfilename(
title="Choose a report",
filetypes=[("Reports", "*.csv *.xlsx *.pdf"), ("All files", "*.*")],
)
if not path:
return
src = Path(path)
size_mb = src.stat().st_size / 1_048_576
if src.suffix.lower() not in ALLOWED:
show(f"{src.name}: {src.suffix} files aren't allowed", "red")
elif size_mb > MAX_MB:
show(f"{src.name} is {size_mb:.1f} MB; the limit is {MAX_MB} MB", "red")
elif (UPLOAD_DIR / src.name).exists():
show(f"{src.name} was already uploaded", "darkorange")
else:
shutil.copy2(src, UPLOAD_DIR / src.name)
show(f"Uploaded {src.name}", "green")
root = tk.Tk()
root.title("Upload a Report")
root.geometry("440x140")
tk.Button(root, text="Upload Report", width=16, command=upload_file).pack(pady=20)
status = tk.Label(root, text="CSV, XLSX or PDF, up to 5 MB")
status.pack()
root.mainloop()

In my run, the .exe was rejected first, the PDF went through in green, and picking that same PDF again gave the orange “already uploaded” message. For the size check itself, see getting a file size in Python.
I prefer a status label to a pop-up because it doesn’t block the user. If you want a pop-up anyway, use the dialogs from the Tkinter messagebox tutorial.
Upload multiple files in Tkinter
askopenfilenames() (with an s) returns a tuple of paths, so the user can Ctrl+click several files. A ttk.Treeview makes a tidy upload list:
import shutil
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, ttk
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
def upload_files():
paths = filedialog.askopenfilenames(title="Choose files to upload")
for path in paths: # empty tuple if the user cancels
src = Path(path)
shutil.copy2(src, UPLOAD_DIR / src.name)
tree.insert("", "end", values=(src.name, f"{src.stat().st_size / 1024:,.1f} KB", "Uploaded"))
summary.config(text=f"{len(paths)} file(s) uploaded to {UPLOAD_DIR}/")
root = tk.Tk()
root.title("Upload Files")
root.geometry("480x230")
ttk.Button(root, text="Choose Files...", command=upload_files).pack(pady=10)
tree = ttk.Treeview(root, columns=("name", "size", "status"), show="headings", height=4)
for col, text, width in (("name", "File", 200), ("size", "Size", 110), ("status", "Status", 100)):
tree.heading(col, text=text)
tree.column(col, width=width, anchor="w")
tree.pack(padx=10)
summary = ttk.Label(root, text="")
summary.pack(pady=6)
root.mainloop()

Cancel returns an empty tuple, so the loop simply doesn’t run. More column, sorting and scrollbar options are in the Tkinter Treeview tutorial.
Upload an image and show a preview in Tkinter
For pictures, users expect to see what they uploaded. Pillow opens the file, makes a thumbnail, and ImageTk.PhotoImage puts it in a label:
import shutil
import tkinter as tk
from pathlib import Path
from tkinter import filedialog
from PIL import Image, ImageTk
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
def upload_image():
path = filedialog.askopenfilename(filetypes=[("Images", "*.png *.jpg *.jpeg *.gif")])
if not path:
return
src = Path(path)
with Image.open(src) as img:
width, height = img.size
img.thumbnail((320, 220)) # preview only; the full file is copied
photo = ImageTk.PhotoImage(img)
preview.config(image=photo)
preview.image = photo # keep a reference or the image disappears
shutil.copy2(src, UPLOAD_DIR / src.name)
caption.config(text=f"Uploaded {src.name} ({width} x {height} px)")
root = tk.Tk()
root.title("Upload an Image")
root.geometry("380x330")
tk.Button(root, text="Upload Image", command=upload_image).pack(pady=10)
preview = tk.Label(root)
preview.pack()
caption = tk.Label(root, text="")
caption.pack(pady=6)
root.mainloop()

Don’t skip preview.image = photo. Without a saved reference, Python frees the image and the label goes blank. Other ways to show pictures are in adding images in Tkinter.
Upload a file from Tkinter to a web server
If “upload” means sending the file to a website or API, use requests.post() with files=. To keep the example self-contained, it starts a tiny local server that reports what it received:
import email
import json
import threading
from email import policy
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
class UploadHandler(BaseHTTPRequestHandler): # stand-in for your real upload endpoint
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
raw = f"Content-Type: {self.headers['Content-Type']}\r\n\r\n".encode() + body
msg = email.message_from_bytes(raw, policy=policy.default)
files = [{"field": p.get_param("name", header="content-disposition"),
"filename": p.get_filename(), "bytes": len(p.get_payload(decode=True))}
for p in msg.iter_parts() if p.get_filename()]
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"saved": files}).encode())
def log_message(self, *args): # keep the console quiet
pass
server = HTTPServer(("127.0.0.1", 8765), UploadHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
def upload_to_server(path, url="http://127.0.0.1:8765/upload"):
with open(path, "rb") as f:
response = requests.post(url, files={"file": (path, f, "text/csv")},
data={"uploaded_by": "emily.carter"}, timeout=30)
response.raise_for_status()
return response.status_code, response.json()
status, reply = upload_to_server("sales_q3_2026.csv")
print("HTTP", status)
print(json.dumps(reply, indent=2))
server.shutdown()
Output:
HTTP 200
{
"saved": [
{
"field": "file",
"filename": "sales_q3_2026.csv",
"bytes": 965
}
]
}

In your app, replace the URL with your real endpoint and call upload_to_server(path) from the button’s function. For big files, run it in a threading.Thread so the window doesn’t freeze.
While a long upload runs, a Tkinter progress bar tells the user it’s still working. Update widgets from the main thread with root.after().
Tkinter upload methods compared
| Task | Use | Returns / does |
|---|---|---|
| Pick one file | filedialog.askopenfilename() | A path string, '' on Cancel |
| Pick several files | filedialog.askopenfilenames() | A tuple of paths, () on Cancel |
| Upload to a folder | shutil.copy2(src, dest) | Copies data and timestamps |
| Upload to a server | requests.post(url, files=...) | Multipart form upload |
| Preview an image | ImageTk.PhotoImage(img) | Image for a Label |
To keep building file features into a Tkinter app, read these:
- Read a text file in Python Tkinter
- Save text to a file in Python Tkinter
- Drag and drop in Python Tkinter
- Copy a file in Python
Frequently asked questions
How do I upload a file in Python Tkinter?
Get a path with filedialog.askopenfilename(), then copy it with shutil.copy2() or send it with requests.post(url, files=...).
What does askopenfilename return if the user cancels?
An empty string, so check if not path: before using it.
How do I upload multiple files in Tkinter?
Use filedialog.askopenfilenames(), which returns a tuple of paths, and loop over it.
How do I restrict the upload to certain file types?
Pass filetypes=[("Reports", "*.csv *.pdf")], and check Path(path).suffix in code too.
Why does my uploaded image not show in Tkinter?
The PhotoImage was garbage collected. Keep a reference, such as label.image = photo.
Where is tkinter.filedialog documented?
See tkinter.filedialog in the Python docs.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn