DEV Community

Malik Ali Hassan
Malik Ali Hassan

Posted on

I Wrote a Python Script That Cleans Up Any Messy Folder — and It Never Deletes Anything by Default

Everyone has that one folder. Downloads, Desktop, some project directory — hundreds of files, no structure, and the thought of sorting it by hand is so dull you just... don't.

I wrote a small Python script that does it for you. It sorts a folder's files into subfolders by type, and the important part is in the title: by default, it changes nothing. It only shows you what it would do. You have to explicitly tell it to move files.

What it does

Point it at a folder and it sorts every file into one of six categories:

Folder Extensions
Images .jpg, .jpeg, .png, .gif, .webp, .svg
Documents .pdf, .doc, .docx, .txt, .md
Spreadsheets .csv, .xls, .xlsx, .ods
Archives .zip, .tar, .gz, .rar, .7z
Code .py, .js, .html, .css, .json
Others anything else

No dependencies beyond the standard library — just argparse, shutil, and pathlib. No install step at all.

Usage

python file_organizer.py ~/Downloads            # dry run, no changes
python file_organizer.py ~/Downloads --apply    # actually move files
Enter fullscreen mode Exit fullscreen mode

The dry run prints lines like would move: report.pdf -> Documents/ and finishes with a count. Only --apply performs the moves.

How it works

The whole script is built around one mapping and one function:

CATEGORIES = {
    "Images": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"},
    "Documents": {".pdf", ".doc", ".docx", ".txt", ".md"},
    "Spreadsheets": {".csv", ".xls", ".xlsx", ".ods"},
    "Archives": {".zip", ".tar", ".gz", ".rar", ".7z"},
    "Code": {".py", ".js", ".html", ".css", ".json"},
}

def category_for(path):
    """Return the category folder name for a file's extension."""
    for name, extensions in CATEGORIES.items():
        if path.suffix.lower() in extensions:
            return name
    return "Others"
Enter fullscreen mode Exit fullscreen mode

Extensions are lowercased before matching, so Photo.JPG and photo.jpg land in the same folder. Anything unknown falls into Others rather than being skipped silently — a file you can't find is worse than a file in a misc folder.

The organising function keeps the dry-run and apply paths in one place, so they can never drift apart:

def organize(folder, apply=False):
    folder = Path(folder)
    if not folder.is_dir():
        print(f"error: {folder} is not a folder", file=sys.stderr)
        return 1
    moved = 0
    for path in sorted(folder.iterdir()):
        if not path.is_file():
            continue  # never touch subfolders
        target_dir = folder / category_for(path)
        target = target_dir / path.name
        if apply:
            target_dir.mkdir(exist_ok=True)
            shutil.move(str(path), str(target))
            print(f"moved: {path.name} -> {target_dir.name}/")
        else:
            print(f"would move: {path.name} -> {target_dir.name}/")
        moved += 1
    mode = "moved" if apply else "would move (dry run — use --apply to move)"
    print(f"{moved} file(s) {mode}")
    return 0
Enter fullscreen mode Exit fullscreen mode

Three safety decisions are doing the quiet work here:

  1. Dry run is the default. The destructive action requires an explicit flag. If you run it by accident, nothing happens — you just get a preview.
  2. It never touches subfolders. Only files directly in the folder are moved. Your existing structure stays intact.
  3. It never deletes. Files are moved, not removed. If a category looks wrong, everything is still there, one folder away.

Three lessons I'd pass on

  1. Make the safe behaviour the default. A script that moves files should have to be talked into it. Dry-run-first is a design decision, not a feature — and it's the reason I'd trust this on a real Downloads folder.
  2. Small scripts beat small apps. This is about 70 lines and has no dependencies. It doesn't need a GUI, a config file, or a scheduler to be useful.
  3. Print what you're doing. Every move gets a line of output, and the run ends with a count. When a script touches your files, silence is a bug.

This is the second tool in my Python Automation Toolkit — the first scrapes a catalogue into a clean Excel report, and this one tidies the folder that report lands in. Next, I'll write about the project that taught me the most: a RAG-powered assistant that answers questions about Pakistani electricity bills.


Malik Ali Hassan — .NET & full-stack developer, final-year Software Engineering student. I build Python automation, AI assistants, and web apps.

Top comments (0)