I'm building a CLI tool that scans a directory and sorts files into folders by type. Small in scope, but every small tool still forces real design decisions. Here are three from this week, and the reasoning behind each.
pathlib over os.path
I chose pathlib
over os.path
for one core reason: paths aren't strings, and treating them as strings invites bugs a purpose-built API already solves. os.path
returns plain strings, so any extension logic has to be hand-rolled, and compound extensions like archive.tar.gz
are where that breaks down.
name, ext = os.path.splitext("archive.tar.gz")
Path("archive.tar.gz").suffix # '.gz' — last suffix only
Path("archive.tar.gz").suffixes # ['.tar', '.gz'] — all of them, if you ask for them
pathlib
doesn't make compound extensions disappear as a problem — .suffix
still only gives you the last one. What it gives you is a typed, documented way to choose which behaviour you want (.suffix
vs .suffixes
) instead of guessing at string-split indices and finding out you guessed wrong when a .tar.gz
file quietly gets misclassified.
Why dry run exist before any destructive operation?
I added a dry-run flag because moving files isn't trivially reversible, and previewing an operation is free. On a personal machine with well-understood files, a wrong move is a minor inconvenience. But the same operation gets genuinely risky once files are shared, moved across filesystems (where the operation isn't atomic), or simply irreplaceable. The flag doesn't assume which situation you're in; it just makes the cost of finding out in advance effectively zero.
$ python3 main.py --source ~/downloads --dry-run
[DRY RUN] invoice.pdf -> Documents/
[DRY RUN] photo.jpg -> Images/
[DRY RUN] script.py -> Code/
A dry run is only trustworthy if it can't lie to you. The branch that decides what file goes where runs identically in both modes; classification happens before the dry-run check, not inside it. The only thing the flag changes is the last step: print the destination instead of calling the move. Same logic, same output shape, one line different.
A dict, not an if/elif chain
An if/elif chain fuses data and logic — adding a new file type means editing the function's control flow directly, which risks breaking existing, already-tested branches.
if ext == ".pdf":
return "Documents"
elif ext == ".jpg":
return "Images"
elif ext == ".py":
return "Code"
EXTENSION_MAP = {".pdf": "Documents", ".jpg": "Images", ".py": "Code"}
return EXTENSION_MAP.get(ext, "Other")
The practical payoff isn't just tidiness. Because the mapping is plain data, it doesn't have to stay Python code at all; it's one small step away from living in a config.json
a non-programmer could edit, instead of being buried inside a function only I can safely touch.
None of these decisions is novel on their own. What made them worth writing down is the habit of asking why before writing the obvious version, the one that works today but quietly costs more later.