Sythra

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.

vaibhavkothari· Aug 26, 2026· 6 min readBeginner
ShareXLinkedIn
Auto-organize your downloads folder with Python cover

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

  1. A script that sorts files into Images, Documents, Video and so on
  2. A dry-run mode that only prints its plan
  3. Safe renaming when a file with that name already exists
  4. Optional date-based subfolders
  5. 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 / Linuxcrontab -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)

ProblemFix
PermissionErrorFile is open in another app; close it and rerun
Corrupted downloadsAdd the is_ready check from Step 6
Script moves its own foldersSkip directories with item.is_file()
Hidden files disappearSkip names starting with .
Watcher fires too earlyIncrease the time.sleep in on_created
Nothing happensdry_run is still True — that is intentional

What you learned

  • pathlib for 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.

Next reading on Sythra Articles

Keep reading

Related articles

Newsletter

Get new articles

Free essays on learning, Python, and ML — no account required. We’ll only email when there’s something worth reading.