Introduction
If your Downloads folder looks like a digital landfill — a chaotic mix of PDFs, screenshots, ZIP archives, Excel files, and random executables — you are not alone. Manually sorting files is one of those tedious tasks that screams out for automation.
In this guide you will build a File Organizer web app using Python and Streamlit. The app scans any folder on your computer, groups files by their type, shows you a preview of the planned moves, and then sorts everything into clean, categorised subfolders with one click. A built-in undo log lets you reverse the operation if needed.
This project complements other automation-focused tutorials on this blog such as the Password Generator and the Weather App — both great examples of turning a Python script into a useful interactive tool.
What You Will Build
The finished app will:
Prerequisites
pip install streamlitAll other libraries used (os, shutil, pathlib, json) are part of the Python standard library — no extra installs required.
File Category Mapping
The organizer groups files by extension. Here is the complete mapping used in this project:
| Category | Extensions |
|---|---|
| Images | .jpg .jpeg .png .gif .bmp .svg .webp .ico .tiff |
| Documents | .pdf .doc .docx .txt .xls .xlsx .ppt .pptx .odt .csv |
| Videos | .mp4 .mov .avi .mkv .wmv .flv .webm .m4v |
| Audio | .mp3 .wav .flac .aac .ogg .m4a .wma |
| Archives | .zip .tar .gz .rar .7z .bz2 .xz |
| Code | .py .js .ts .html .css .java .cpp .c .json .yaml .xml .sh |
| Executables | .exe .dmg .pkg .deb .rpm .msi .app |
| Data | .db .sqlite .sql .csv .parquet .feather |
| Others | Everything else |
Project Structure
file_organizer/
├── file_organizer.py ← Main Streamlit app
├── undo_log.json ← Auto-generated undo history
└── requirements.txtStep 1: Define the Category Map
Create file_organizer.py and start with the extension-to-category mapping:
import os
import shutil
import json
from pathlib import Path
import streamlit as st
st.set_page_config(page_title="File Organizer", page_icon="📂", layout="wide")
st.title("📂 File Organizer")
st.write("Automatically sort your files into categorised folders — preview first, move when ready.")
CATEGORY_MAP = {
"Images": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg",
".webp", ".ico", ".tiff"],
"Documents": [".pdf", ".doc", ".docx", ".txt", ".xls", ".xlsx",
".ppt", ".pptx", ".odt", ".csv"],
"Videos": [".mp4", ".mov", ".avi", ".mkv", ".wmv", ".flv",
".webm", ".m4v"],
"Audio": [".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a", ".wma"],
"Archives": [".zip", ".tar", ".gz", ".rar", ".7z", ".bz2", ".xz"],
"Code": [".py", ".js", ".ts", ".html", ".css", ".java", ".cpp",
".c", ".json", ".yaml", ".xml", ".sh"],
"Executables": [".exe", ".dmg", ".pkg", ".deb", ".rpm", ".msi", ".app"],
"Data": [".db", ".sqlite", ".sql", ".parquet", ".feather"],
}
def get_category(extension: str) -> str:
ext = extension.lower()
for category, extensions in CATEGORY_MAP.items():
if ext in extensions:
return category
return "Others"Step 2: Scan the Folder
The scanner collects all files (not folders) in the target directory and returns a list of dicts with metadata:
def scan_folder(folder_path: str) -> list[dict]:
"""Return a list of file info dicts for all files in the given folder."""
path = Path(folder_path)
files = []
for item in path.iterdir():
if item.is_file():
size_bytes = item.stat().st_size
files.append({
"name": item.name,
"extension": item.suffix.lower() or "(none)",
"category": get_category(item.suffix),
"size_kb": round(size_bytes / 1024, 1),
"source": str(item),
"dest": str(path / get_category(item.suffix) / item.name),
})
return sorted(files, key=lambda f: f["category"])Note: item.iterdir() only lists the top level of the folder — it does not recurse into subfolders. Add rglob("*") instead of iterdir() if you want recursive scanning.
Step 3: Build the Folder Input and Scan UI
st.divider()
st.subheader("📁 Step 1 — Choose a Folder")
folder_input = st.text_input(
"Enter the full path to the folder you want to organise:",
placeholder="e.g. /Users/you/Downloads or C:\\Users\\you\\Downloads",
)
dry_run = st.toggle("🔍 Dry Run (preview only — do not move files)", value=True)
if st.button("🔎 Scan Folder", type="primary") and folder_input:
if not os.path.isdir(folder_input):
st.error("❌ That path does not exist or is not a folder. Please check and try again.")
else:
files = scan_folder(folder_input)
st.session_state["scanned_files"] = files
st.session_state["target_folder"] = folder_input
st.success(f"✅ Found **{len(files)} files** in `{folder_input}`")Step 4: Display the Preview Table
After scanning, show a filterable preview of what will be moved:
if "scanned_files" in st.session_state:
files = st.session_state["scanned_files"]
st.divider()
st.subheader("📋 Step 2 — Preview")
# Category filter
categories = ["All"] + sorted(set(f["category"] for f in files))
selected_cat = st.selectbox("Filter by category:", categories)
filtered = files if selected_cat == "All" else [
f for f in files if f["category"] == selected_cat
]
# Summary metrics
col1, col2, col3 = st.columns(3)
col1.metric("Total Files", len(files))
col2.metric("Categories", len(set(f["category"] for f in files)))
col3.metric(
"Total Size",
f"{sum(f['size_kb'] for f in files) / 1024:.1f} MB"
)
# Preview table
st.dataframe(
[{"File": f["name"], "Category": f["category"],
"Extension": f["extension"], "Size (KB)": f["size_kb"]}
for f in filtered],
use_container_width=True,
hide_index=True,
)Step 5: Move Files
The core organizer function creates subfolders and moves each file:
def organize_files(files: list[dict], dry_run: bool) -> list[dict]:
"""Move files to their category subfolders. Returns a log of moves made."""
log = []
for f in files:
dest_dir = Path(f["dest"]).parent
if not dry_run:
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.move(f["source"], f["dest"])
log.append({
"file": f["name"],
"from": f["source"],
"to": f["dest"],
"dry_run": dry_run,
})
return logConnect this to the UI:
st.divider()
st.subheader("🚀 Step 3 — Organise")
action_label = "🔍 Simulate (Dry Run)" if dry_run else "📁 Organise Files"
if st.button(action_label, type="primary", use_container_width=True):
log = organize_files(files, dry_run)
# Save undo log to JSON
if not dry_run:
undo_path = Path(st.session_state["target_folder"]) / "_organizer_undo.json"
with open(undo_path, "w") as f_json:
json.dump(log, f_json, indent=2)
st.success(f"✅ Organised {len(log)} files! Undo log saved to `_organizer_undo.json`.")
else:
st.info(f"🔍 Dry run complete — {len(log)} files would be moved. Toggle off Dry Run to apply.")
st.session_state["last_log"] = logStep 6: Undo Functionality
The undo feature reads the JSON log and moves each file back to its original location:
def undo_organize(log: list[dict]) -> int:
"""Move files back to their original locations using the undo log."""
count = 0
for entry in log:
src = Path(entry["to"])
dst = Path(entry["from"])
if src.exists():
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
count += 1
return countAdd this to the UI after the organise section:
st.divider()
st.subheader("↩️ Undo")
undo_file = st.file_uploader(
"Upload an `_organizer_undo.json` file to revert a previous organisation:",
type=["json"],
)
if undo_file and st.button("↩️ Undo Last Organisation"):
log_data = json.load(undo_file)
moved_back = undo_organize(log_data)
st.success(f"✅ Reverted {moved_back} files to their original locations.")Complete file_organizer.py
import os, shutil, json
from pathlib import Path
import streamlit as st
st.set_page_config(page_title="File Organizer", page_icon="📂", layout="wide")
st.title("📂 File Organizer")
CATEGORY_MAP = {
"Images": [".jpg",".jpeg",".png",".gif",".bmp",".svg",".webp",".ico",".tiff"],
"Documents": [".pdf",".doc",".docx",".txt",".xls",".xlsx",".ppt",".pptx",".odt"],
"Videos": [".mp4",".mov",".avi",".mkv",".wmv",".flv",".webm",".m4v"],
"Audio": [".mp3",".wav",".flac",".aac",".ogg",".m4a",".wma"],
"Archives": [".zip",".tar",".gz",".rar",".7z",".bz2",".xz"],
"Code": [".py",".js",".ts",".html",".css",".java",".cpp",".c",".json",".yaml"],
"Executables": [".exe",".dmg",".pkg",".deb",".rpm",".msi",".app"],
}
def get_category(ext):
ext = ext.lower()
for cat, exts in CATEGORY_MAP.items():
if ext in exts: return cat
return "Others"
def scan_folder(folder):
p = Path(folder)
return sorted([
{"name": i.name, "extension": i.suffix.lower() or "(none)",
"category": get_category(i.suffix),
"size_kb": round(i.stat().st_size/1024, 1),
"source": str(i), "dest": str(p/get_category(i.suffix)/i.name)}
for i in p.iterdir() if i.is_file()
], key=lambda f: f["category"])
def organize_files(files, dry_run):
log = []
for f in files:
dest_dir = Path(f["dest"]).parent
if not dry_run:
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.move(f["source"], f["dest"])
log.append({"file": f["name"], "from": f["source"], "to": f["dest"]})
return log
def undo_organize(log):
count = 0
for e in log:
src, dst = Path(e["to"]), Path(e["from"])
if src.exists():
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
count += 1
return count
# --- UI ---
folder_input = st.text_input("📁 Folder path:", placeholder="/Users/you/Downloads")
dry_run = st.toggle("🔍 Dry Run", value=True)
if st.button("🔎 Scan", type="primary") and folder_input:
if not os.path.isdir(folder_input):
st.error("Path not found.")
else:
files = scan_folder(folder_input)
st.session_state.update({"files": files, "folder": folder_input})
st.success(f"Found {len(files)} files.")
if "files" in st.session_state:
files = st.session_state["files"]
cats = ["All"] + sorted(set(f["category"] for f in files))
sel = st.selectbox("Filter:", cats)
shown = files if sel == "All" else [f for f in files if f["category"]==sel]
c1, c2, c3 = st.columns(3)
c1.metric("Files", len(files))
c2.metric("Categories", len(cats)-1)
c3.metric("Size", f"{sum(f['size_kb'] for f in files)/1024:.1f} MB")
st.dataframe([{"File": f["name"], "Category": f["category"],
"Size (KB)": f["size_kb"]} for f in shown],
use_container_width=True, hide_index=True)
label = "🔍 Simulate" if dry_run else "📁 Organise Files"
if st.button(label, type="primary", use_container_width=True):
log = organize_files(files, dry_run)
if not dry_run:
undo_path = Path(st.session_state["folder"]) / "_organizer_undo.json"
with open(undo_path, "w") as fj: json.dump(log, fj, indent=2)
st.success(f"✅ Organised {len(log)} files!")
else:
st.info(f"🔍 {len(log)} files would be moved.")
st.divider()
undo_file = st.file_uploader("Upload undo JSON", type=["json"])
if undo_file and st.button("↩️ Undo"):
st.success(f"Reverted {undo_organize(json.load(undo_file))} files.")Run the App
streamlit run file_organizer.pyKey Python Standard Library Tools
| Module | Function / Class | What It Does |
|---|---|---|
pathlib | Path(folder) | Cross-platform file paths |
pathlib | path.iterdir() | Lists contents of a directory |
pathlib | path.is_file() | True only for files (not dirs) |
pathlib | path.stat().st_size | File size in bytes |
pathlib | path.suffix | File extension, e.g. .pdf |
pathlib | path.mkdir(parents=True) | Creates folder (and parents) |
shutil | shutil.move(src, dst) | Moves a file to a new location |
json | json.dump() / json.load() | Saves and loads the undo log |
os | os.path.isdir() | Validates that a path is a folder |
Comparing Approaches: shutil vs pathlib rename
When moving files in Python you have two main options:
| Approach | Code | Cross-device? | Notes |
|---|---|---|---|
shutil.move() | shutil.move(src, dst) | ✅ Yes | Works across drives; preferred |
Path.rename() | Path(src).rename(dst) | ❌ No | Fast but only same filesystem |
Path.replace() | Path(src).replace(dst) | ❌ No | Overwrites destination silently |
Always use shutil.move() when the destination might be on a different drive or network share.
Safety Best Practices
| Risk | Mitigation in This App |
|---|---|
| Accidentally moving important files | Dry run toggle on by default |
| Overwriting existing files at destination | Unique filenames are preserved; add a counter suffix if needed |
| Moving already-organised subfolders | path.is_file() skips directories |
| No way to recover | Undo log saved as JSON before every operation |
| Path doesn't exist | os.path.isdir() check before scanning |
Extending the File Organizer
| Feature | How to Implement |
|---|---|
| Recursive scanning | Replace iterdir() with rglob("*") |
| Custom category rules | Add a UI form to define custom extension→category mappings |
| Duplicate detection | Hash files with hashlib.md5() and flag duplicates |
| Schedule automatic runs | Use schedule library or a cron job |
| Email report | Send the move summary via smtplib |
| Dark/light mode UI | Already handled by Streamlit's built-in theme switcher |
Related Projects
requests and external APIs to automate data retrieval.st.session_state for multi-step workflows, the same pattern used in this organizer.Conclusion
You have built a fully functional, safe, and reversible File Organizer in Python and Streamlit. The app uses only the Python standard library (plus Streamlit) — no heavy dependencies, no cloud services, no API keys. The dry-run mode and undo log make it production-safe enough to run on your real Downloads folder.
Resources: