The Quiet Design Choices Behind a 50-Line CLI Tool A developer building a 50-line CLI tool that sorts files by type shared three design decisions: using pathlib over os.path for robust extension handling, implementing a dry-run flag before destructive operations, and using a dict instead of an if/elif chain for maintainability. The choices prioritize explicit behavior, safety, and separation of data from logic. 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. os.path — a naive split silently mishandles compound extensions name, ext = os.path.splitext "archive.tar.gz" ext == '.gz' — the '.tar' part is just gone unless you call splitext again pathlib — the same gap exists, but it's explicit, not hidden 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. php $ 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/elif — data and logic tangled together if ext == ".pdf": return "Documents" elif ext == ".jpg": return "Images" elif ext == ".py": return "Code" every new type = another branch, edited into working code dict — data and logic separated EXTENSION MAP = {".pdf": "Documents", ".jpg": "Images", ".py": "Code"} return EXTENSION MAP.get ext, "Other" every new type = one line added to a table; the lookup never changes 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.