Article
Auto-organize your downloads folder with Python
A script that sorts files into folders by type, handles name clashes safely, and can keep watching for new arrivals. Dry-run first, so nothing is ever lost.
On this page0%
- What you will build
- Step 1 — Look before you touch
- Step 2 — Map extensions to folders
- Step 3 — Never overwrite a file
- Step 4 — Move files, dry-run first
- Step 5 — Add date subfolders
- Step 6 — Handle files still downloading
- Step 7 — Watch the folder continuously
- Step 8 — Run it automatically
- Common problems (and fixes)
- What you learned
- FAQ
- How do I automatically organize files by type in Python?
- How do I avoid overwriting files when moving them?
- How do I stop the script moving half-downloaded files?
- Can Python watch a folder and sort files automatically?
- How do I run a Python script on a schedule?
- Next reading on Sythra Articles
Your Downloads folder has 900 files. Screenshots, invoices, three copies of the same installer, a dataset you needed once.
Twenty lines of Python can sort it into folders in under a second — and with a dry run first, so you see exactly what will happen before anything moves.
What you will build
- A script that sorts files into
Images,Documents,Videoand so on - A dry-run mode that only prints its plan
- Safe renaming when a file with that name already exists
- Optional date-based subfolders
- An optional watcher that files new downloads as they arrive
Step 1 — Look before you touch
from pathlib import Path
from collections import Counter
folder = Path.home() / "Downloads"
files = [f for f in folder.iterdir() if f.is_file()]
print(f"{len(files)} files")
extensions = Counter(f.suffix.lower() for f in files)
for ext, count in extensions.most_common(15):
print(f"{ext or '(none)':10} {count}")
Path.home() finds your home directory on any operating system, so this script works unchanged on Windows, macOS and Linux. Run this first — the output tells you which categories you actually need.
Step 2 — Map extensions to folders
CATEGORIES = {
"Images": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".heic", ".bmp"},
"Documents": {".pdf", ".docx", ".doc", ".txt", ".md", ".odt", ".rtf"},
"Spreadsheets": {".xlsx", ".xls", ".csv", ".ods"},
"Slides": {".pptx", ".ppt", ".odp"},
"Video": {".mp4", ".mov", ".avi", ".mkv", ".webm"},
"Audio": {".mp3", ".wav", ".m4a", ".flac", ".aac"},
"Archives": {".zip", ".rar", ".7z", ".tar", ".gz"},
"Installers": {".exe", ".msi", ".dmg", ".deb", ".pkg"},
"Code": {".py", ".js", ".ts", ".ipynb", ".json", ".html", ".css"},
}
def category_for(path):
suffix = path.suffix.lower()
for name, extensions in CATEGORIES.items():
if suffix in extensions:
return name
return "Other"
Sets are used deliberately: in on a set is instant regardless of size, unlike scanning a list.
Step 3 — Never overwrite a file
The dangerous part of any move script. If report.pdf already exists in the destination, shutil.move will happily replace it. Add a counter instead:
def unique_path(target):
if not target.exists():
return target
stem, suffix, parent = target.stem, target.suffix, target.parent
counter = 2
while True:
candidate = parent / f"{stem} ({counter}){suffix}"
if not candidate.exists():
return candidate
counter += 1
report.pdf becomes report (2).pdf. Nothing is ever lost — the same safety idea as the dry-run mode in our file renaming guide.
Step 4 — Move files, dry-run first
import shutil
from pathlib import Path
def organize(folder, dry_run=True):
folder = Path(folder)
moved = 0
for item in folder.iterdir():
if not item.is_file() or item.name.startswith("."):
continue
destination_dir = folder / category_for(item)
destination = unique_path(destination_dir / item.name)
if dry_run:
print(f"[dry-run] {item.name} -> {destination_dir.name}/{destination.name}")
else:
destination_dir.mkdir(exist_ok=True)
shutil.move(str(item), str(destination))
print(f"[moved] {item.name} -> {destination_dir.name}/{destination.name}")
moved += 1
print(f"\n{moved} files {'would be ' if dry_run else ''}organized.")
if __name__ == "__main__":
organize(Path.home() / "Downloads", dry_run=True)
dry_run=True is the default on purpose. Read the output, confirm it looks right, then flip it to False.
Two guards matter more than they look. Skipping directories stops the script trying to move the category folders it just created. Skipping names starting with . protects hidden system files.
Step 5 — Add date subfolders
Useful for screenshots and invoices, where you remember roughly when rather than what:
from datetime import datetime
def dated_destination(folder, item, by_date=False):
base = folder / category_for(item)
if not by_date:
return base
modified = datetime.fromtimestamp(item.stat().st_mtime)
return base / f"{modified:%Y}" / f"{modified:%m-%B}"
This produces Images/2026/08-August/photo.jpg. Create nested folders with mkdir(parents=True, exist_ok=True).
Step 6 — Handle files still downloading
Moving a half-downloaded file corrupts it. Skip browser temp files and anything modified in the last few seconds:
import time
SKIP_SUFFIXES = {".crdownload", ".part", ".tmp", ".download"}
def is_ready(item, quiet_seconds=5):
if item.suffix.lower() in SKIP_SUFFIXES:
return False
return (time.time() - item.stat().st_mtime) > quiet_seconds
Call is_ready(item) in the loop and continue when it returns False.
Step 7 — Watch the folder continuously
python -m pip install watchdog
import time
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class Sorter(FileSystemEventHandler):
def __init__(self, folder):
self.folder = Path(folder)
def on_created(self, event):
if event.is_directory:
return
item = Path(event.src_path)
time.sleep(3) # let the download finish
if not item.exists() or not is_ready(item):
return
destination_dir = self.folder / category_for(item)
destination_dir.mkdir(exist_ok=True)
shutil.move(str(item), str(unique_path(destination_dir / item.name)))
print(f"Filed: {item.name} -> {destination_dir.name}/")
if __name__ == "__main__":
downloads = Path.home() / "Downloads"
observer = Observer()
observer.schedule(Sorter(downloads), str(downloads), recursive=False)
observer.start()
print("Watching. Ctrl+C to stop.")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
Now every new download files itself. recursive=False keeps it watching only the top level, so it ignores the folders it creates.
Step 8 — Run it automatically
Windows — Task Scheduler → Create Basic Task → Daily → Start a program:
Program: C:\Path\To\.venv\Scripts\python.exe
Arguments: C:\Path\To\organize.py
macOS / Linux — crontab -e, then run daily at 6pm:
0 18 * * * /path/to/.venv/bin/python /path/to/organize.py
Use the full path to the Python inside your environment. A bare python in a scheduled task often resolves to a different interpreter — the same class of problem behind 'pip' is not recognized.
Common problems (and fixes)
| Problem | Fix |
|---|---|
PermissionError | File is open in another app; close it and rerun |
| Corrupted downloads | Add the is_ready check from Step 6 |
| Script moves its own folders | Skip directories with item.is_file() |
| Hidden files disappear | Skip names starting with . |
| Watcher fires too early | Increase the time.sleep in on_created |
| Nothing happens | dry_run is still True — that is intentional |
What you learned
pathlibfor paths that work on every operating system- Sets for instant extension lookup
- Safe renaming instead of overwriting
- Dry-run as a default for anything destructive
- Watching a folder for changes with
watchdog - Scheduling a script to run on its own
FAQ
How do I automatically organize files by type in Python?
Loop over the folder with pathlib, map each file's extension to a category, create the category folder with mkdir(exist_ok=True), and move the file with shutil.move. Run it in dry-run mode first.
How do I avoid overwriting files when moving them?
Check whether the destination exists and, if it does, append a counter to the filename — report (2).pdf — until you find a name that is free.
How do I stop the script moving half-downloaded files?
Skip .crdownload, .part and .tmp suffixes, and ignore any file modified within the last few seconds.
Can Python watch a folder and sort files automatically?
Yes. Install watchdog, subclass FileSystemEventHandler, and handle on_created with a short delay so the download finishes before the file is moved.
How do I run a Python script on a schedule?
Use Task Scheduler on Windows or cron on macOS and Linux, and give the full path to the Python interpreter inside your virtual environment rather than a bare python.