<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Charles Adesoba</title>
    <description>The latest articles on DEV Community by Charles Adesoba (@carl_yo).</description>
    <link>https://dev.to/carl_yo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071578%2Ffe379956-830f-4fa2-ac8c-26778784762f.png</url>
      <title>DEV Community: Charles Adesoba</title>
      <link>https://dev.to/carl_yo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carl_yo"/>
    <language>en</language>
    <item>
      <title>The Quiet Design Choices Behind a 50-Line CLI Tool</title>
      <dc:creator>Charles Adesoba</dc:creator>
      <pubDate>Mon, 10 Aug 2026 18:55:13 +0000</pubDate>
      <link>https://dev.to/carl_yo/the-quiet-design-choices-behind-a-50-line-cli-tool-1ci2</link>
      <guid>https://dev.to/carl_yo/the-quiet-design-choices-behind-a-50-line-cli-tool-1ci2</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pathlib over os.path&lt;/strong&gt;&lt;br&gt;
I chose &lt;code&gt;pathlib&lt;/code&gt; over &lt;code&gt;os.path&lt;/code&gt; for one core reason: paths aren't strings, and treating them as strings invites bugs a purpose-built API already solves. &lt;code&gt;os.path&lt;/code&gt; returns plain strings, so any extension logic has to be hand-rolled, and compound extensions like &lt;code&gt;archive.tar.gz&lt;/code&gt; are where that breaks down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# os.path — a naive split silently mishandles compound extensions
&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;archive.tar.gz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ext == '.gz'  — the '.tar' part is just gone unless you call splitext again
&lt;/span&gt;
&lt;span class="c1"&gt;# pathlib — the same gap exists, but it's explicit, not hidden
&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;archive.tar.gz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;     &lt;span class="c1"&gt;# '.gz'   — last suffix only
&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;archive.tar.gz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;suffixes&lt;/span&gt;   &lt;span class="c1"&gt;# ['.tar', '.gz']  — all of them, if you ask for them
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pathlib&lt;/code&gt; doesn't make compound extensions disappear as a problem — &lt;code&gt;.suffix&lt;/code&gt; still only gives you the last one. What it gives you is a typed, documented way to choose which behaviour you want (&lt;code&gt;.suffix&lt;/code&gt; vs &lt;code&gt;.suffixes&lt;/code&gt;) instead of guessing at string-split indices and finding out you guessed wrong when a &lt;code&gt;.tar.gz&lt;/code&gt; file quietly gets misclassified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why dry run exist before any destructive operation?&lt;/strong&gt;&lt;br&gt;
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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 main.py &lt;span class="nt"&gt;--source&lt;/span&gt; ~/downloads &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;span class="gp"&gt;[DRY RUN] invoice.pdf -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Documents/
&lt;span class="gp"&gt;[DRY RUN] photo.jpg -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Images/
&lt;span class="gp"&gt;[DRY RUN] script.py -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Code/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dict, not an if/elif chain&lt;/strong&gt;&lt;br&gt;
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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# if/elif — data and logic tangled together
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# every new type = another branch, edited into working code
&lt;/span&gt;
&lt;span class="c1"&gt;# dict — data and logic separated
&lt;/span&gt;&lt;span class="n"&gt;EXTENSION_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EXTENSION_MAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# every new type = one line added to a table; the lookup never changes
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;config.json&lt;/code&gt; a non-programmer could edit, instead of being buried inside a function only I can safely touch.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
