The problem
Sorting files by hand is boring, and it is the kind of job I keep putting off. So I built a small script that does the sorting for me.
What I built
tidy.py is a short Python script that asks a local open-weight model to sort the files in a folder into subfolders: documents, spreadsheets, images, videos, code, archives, installers and other.
- The model is Qwen 2.5 3B, run locally through Ollama.
- It uses only the Python standard library, so there is nothing else to install.
- It does a dry run by default: it prints a plan and moves nothing until I add
--apply. - It never deletes anything. It only moves files into subfolders, and it skips a file if one with the same name is already in the target folder.
Demo
I tested it on a dummy folder of 12 empty files with realistic names (holiday.jpg, budget.xlsx, sales_data.csv, setup.exe and so on), so nothing real was at risk.
Before: 12 loose files, and the plan the model proposed.
After running with --apply: the same files, sorted into folders.
Why open matters
-
The model runs on my own computer. The script only talks to
localhost, so my file names are never sent to a server I don't control. Once the model is downloaded, nothing in the script needs the internet. (I did not test it with Wi-Fi switched off.) - It costs nothing to run. There is no API key and no per-request fee.
- I can swap the model by changing one line. The model name is a single variable at the top of the script.
-
I can change how it behaves. When
budget.xlsxlanded in "documents", I added a "spreadsheets" category to the list, and the next run sorted it correctly. I could do that because I control the whole setup.
What went wrong
- The first run seemed to hang. I stopped it and ran it again, and it worked. My guess is that the model was loading into memory for the first time, but I did not confirm that.
-
The model only sees file names, not contents. It can't tell what
final_v2.pdfis about. -
It is not always consistent. Even after I added the "spreadsheets" category,
sales_data.csvstill went to "documents". Giving the model a few examples, or checking the file extension in code before asking the model, would probably fix this. - My test was tiny. 12 dummy files is a sanity check, not a benchmark. I'd want to try it on a real, messy folder before trusting it.
How to run it
- Install Ollama and download the model:
ollama pull qwen2.5:3b
- Save the script below as
tidy.py. - Make a dry run on a test folder:
python tidy.py path/to/folder
- If the plan looks right, apply it:
python tidy.py path/to/folder --apply
"""Folder tidier: a local open-weight model (via Ollama) sorts files into folders.
Dry run by default. Never deletes anything; it only moves files into subfolders."""
import json
import shutil
import sys
import urllib.error
import urllib.request
from pathlib import Path
MODEL = "qwen2.5:3b"
CATEGORIES = ["documents", "spreadsheets", "images", "videos", "code", "archives", "installers", "other"]
def ask(filename: str) -> str:
prompt = (f"Classify this file name into exactly one of: {', '.join(CATEGORIES)}.\n"
f"File name: {filename}\nAnswer with the single category word only.")
body = json.dumps({"model": MODEL, "prompt": prompt, "stream": False,
"options": {"temperature": 0}}).encode()
req = urllib.request.Request("http://localhost:11434/api/generate", data=body,
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=120) as r:
answer = json.load(r)["response"].strip().lower()
except urllib.error.URLError:
sys.exit("Could not reach Ollama. Is it running? Try: ollama list")
for category in CATEGORIES:
if category in answer:
return category
return "other"
def main():
if len(sys.argv) < 2:
sys.exit("Usage: python tidy.py <folder> [--apply]")
folder = Path(sys.argv[1]).expanduser()
apply = "--apply" in sys.argv
plan = [(f, folder / ask(f.name)) for f in sorted(folder.iterdir()) if f.is_file()]
for f, dest in plan:
print(f"{f.name:30} -> {dest.name}/")
if apply:
for f, dest in plan:
dest.mkdir(exist_ok=True)
target = dest / f.name
if not target.exists():
shutil.move(str(f), str(target))
print("\nDone.")
else:
print("\nDry run only. Re-run with --apply to move the files.")
main()
A note on AI use
I used an AI assistant (Claude) to help write the script and to draft and edit this post. I ran and tested the script myself, and the results and limits described above are from my own runs.


Top comments (0)