// Write-Up · Desktop Tooling

Sorting Out
Downloads

Building and shipping a file organization utility — from Tkinter prototype to PyQt6 release.

Python · PyQt6
5 min read
Desktop · Packaging

The Problem

The Downloads folder is where order goes to die. Not dramatically — it just quietly accumulates. A PDF from six months ago. A zip file you extracted and forgot to delete. Installers for software you may or may not have actually installed. Three different versions of the same spreadsheet with names like final_FINAL_v3_USE_THIS.xlsx.

I'd cleaned mine out manually enough times to know that the problem wasn't discipline — it was that the friction of sorting files by hand was always higher than just leaving them there. I needed something that would do it for me, preview what it was about to do, and let me undo it if it got it wrong.

The First Build (Tkinter)

The initial version was a Tkinter app — fast to build, functional, and immediately annoying to look at. Tkinter gets you to working faster than almost anything else in Python, but the result feels like something assembled from a 2003 parts bin. It did the core job: scan a folder, group files by extension, move them to typed subdirectories.

The sorting logic was straightforward: map extension groups (documents, images, spreadsheets, scripts, archives, audio, video, executables, fonts) to target folders, then move anything that matched. The harder part was handling edge cases — what happens when a target folder already exists, when a file has no extension, when the user's pointing it at a directory that contains project files they definitely don't want touched.

The PyQt6 Migration (v0.2 → v0.3)

The jump from Tkinter to PyQt6 was the right call and I delayed it longer than I should have. PyQt6 gives you a real UI framework — native-feeling widgets, proper layouts, signals and slots that make sense, and styling that doesn't require fighting the framework.

v0.2 added the features that mattered most in practice: a preview mode that showed you exactly what would move where before anything happened, a one-step undo that reversed the last sort batch, and folder watching via watchdog — so SortShelf could monitor a folder continuously and sort new files as they arrived. That last feature changed how the tool felt to use. It went from "something you run when things get bad" to "something you leave running."

v0.3 added a Scripts category and bumped the default font size — a small change that surfaced from actually using the app daily. Real usage feedback beats imagined user stories every time.

Packaging and Distribution

Getting a Python app to run on a Windows machine that doesn't have Python installed is where projects like this go to die if you're not prepared for it. PyInstaller handles most of the work, but "most" leaves a lot of room for issues.

The main lessons from packaging SortShelf:

  • Hidden imports will find you. PyQt6 has enough dynamically loaded components that you'll almost certainly hit a "module not found" error at runtime that wasn't visible at build time.
  • Test on a clean machine. Your dev machine has things installed that the target machine won't. The only honest test is a clean Windows environment with nothing extra on it.
  • Mark of the Web is a real problem. Files downloaded from the internet get flagged by Windows as potentially unsafe. A PyInstaller bundle downloaded from Gumroad would trigger this by default — stripping the MOTW flag as a build discipline step became standard practice.
  • The single-file build is slower but simpler for the end user. --onefile produces one .exe that self-extracts to %TEMP% on launch. The startup is slower, but it's one file with no install step and no directory to manage.

What It Taught Me

The gap between "working on my machine" and "something another person can use without friction" is larger than any single feature. SortShelf was useful to me from v0.1. It took until v0.3 to be something I'd hand to someone else without caveats.

That gap is mostly packaging, edge case handling, and the kind of UI polish that only shows up when someone who isn't you tries to use the thing. Every real tool I've built since has gone through a deliberate "hand it to a human" step before I'd call it done.

// Have a Problem Worth Solving?

Let's Figure
It Out.

Discovery calls are free and take 30 minutes.