The Quiet Terminal

← Practical Tips

Sorting a messy Downloads folder with a twenty-line script

2026-08-07 · 4 min read

My Downloads folder is where files go to disappear. Installers from three years ago, screenshots named screenshot_0143.png, PDFs I still need, one .csv I exported once and never found again. Search never helps, because I can't remember names — only types.

So I wrote a small script that sorts everything in Downloads into subfolders by extension: pdfs/, images/, archives/, data/. It's a cousin of the monthly PDF cleanup script from my pathlib post, but general-purpose — and writing it taught me two habits I now put into every script that moves files: a dry-run flag, and a plan for name collisions.

The whole script

from pathlib import Path
import shutil

FOLDERS = {
    ".pdf": "pdfs",
    ".png": "images",
    ".jpg": "images",
    ".jpeg": "images",
    ".zip": "archives",
    ".csv": "data",
}

DRY_RUN = True
downloads = Path.home() / "Downloads"

for f in downloads.iterdir():
    if f.is_dir():
        continue
    folder = FOLDERS.get(f.suffix.lower(), "other")
    dest_dir = downloads / folder
    verb = "would move" if DRY_RUN else "moving"
    print(f"{verb}: {f.name} -> {folder}")
    if DRY_RUN:
        continue
    dest_dir.mkdir(exist_ok=True)
    dest = dest_dir / f.name
    n = 1
    while dest.exists():
        dest = dest_dir / f"{f.stem}_{n}{f.suffix}"
        n += 1
    shutil.move(f, dest)

Files with no extension, or an extension I never listed, land in other/ — which is exactly the bucket I want to see grow. It means my mapping needs another entry, not that the script is wrong. The dict is the whole configuration: adding .md files is one line, ".md": "notes".

One deliberate omission: .exe and .dmg installers aren't in the mapping at all. I tried sorting them into an installers/ folder and discovered the honest truth about installers — I never want an old one, I want them gone. Now they fall into other/, the dry run lists them, and I delete them on the spot.

Run it dry first

The first version moved files the moment I ran it, and I spent an evening digging things back out of the wrong folders. Now the script starts with DRY_RUN = True and only prints what it would do:

would move: flight_confirmation.pdf -> pdfs
would move: screenshot_0143.png -> images

I let it run like that for a week, read the output, added the two extensions I'd missed, and only then flipped the flag. I'd call that paranoia, except for the time I skipped it and renamed sixty files straight into a folder that didn't exist yet. A dry run is not paranoia; it's the difference between a cleanup script and a data-loss incident.

Never overwrite a file

The while loop handles collisions. If report.pdf already exists in the target folder, the script tries report_1.pdf, then report_2.pdf, and keeps going until it finds a free name. Nothing gets overwritten, ever — your browser does the same thing when you download the same file twice.

As for shutil.move() instead of rename(): inside Downloads either would work, but rename() fails when source and destination sit on different drives, and shutil.move() just handles it. One less thing to think about on the day I point the script at a second disk.