<?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: 2-glitch</title>
    <description>The latest articles on DEV Community by 2-glitch (@2glitch).</description>
    <link>https://dev.to/2glitch</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%2F3982583%2Fb75a11e7-7693-43f1-9c76-1b46962b21be.png</url>
      <title>DEV Community: 2-glitch</title>
      <link>https://dev.to/2glitch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/2glitch"/>
    <language>en</language>
    <item>
      <title>I Shipped One Messy Python Script. Here's the 10-Point Checklist That Got It There.</title>
      <dc:creator>2-glitch</dc:creator>
      <pubDate>Mon, 15 Jun 2026 05:18:27 +0000</pubDate>
      <link>https://dev.to/2glitch/i-shipped-one-messy-python-script-heres-the-10-point-checklist-that-got-it-there-3eg3</link>
      <guid>https://dev.to/2glitch/i-shipped-one-messy-python-script-heres-the-10-point-checklist-that-got-it-there-3eg3</guid>
      <description>&lt;p&gt;AI writes you a working Python script in about ninety seconds. It runs. You move on.&lt;/p&gt;

&lt;p&gt;But the script has a long afterlife. It picks up a hardcoded &lt;code&gt;Downloads&lt;/code&gt; path. It sprouts a &lt;code&gt;# edit before running!!&lt;/code&gt; comment. Someone copies it to &lt;code&gt;script_v2_FINAL.py&lt;/code&gt;. And six weeks later a teammate asks to use it, feeds it a slightly different CSV, and it dies with a raw traceback — or worse, it silently writes a half-finished file and exits 0.&lt;/p&gt;

&lt;p&gt;That gap between "runs on my machine" and "I'd let someone else run this" — &lt;em&gt;that distance is the actual engineering work&lt;/em&gt;. And AI almost never closes it for you, because you never asked. (That's an observation from watching a lot of generated code, not a measured statistic — but I'd bet you've seen it too.)&lt;/p&gt;

&lt;p&gt;This post is a teardown. I'm going to take one deliberately typical messy script, name every defect, and walk through exactly what changes to ship it. By the end you'll have a 10-point checklist you can run on your own code today, with zero tools beyond Python and pytest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "before" — a script that works exactly once
&lt;/h2&gt;

&lt;p&gt;Here's the real starting script. It totals an expenses CSV by category and flags anything over a limit. It works. That's the trap. (Trimmed in the middle for length; the structure is what matters.)&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;# expense report script v2 FINAL (working!!)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;

&lt;span class="n"&gt;INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Users/me/Downloads/expenses.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;- edit this before running!!
&lt;/span&gt;&lt;span class="n"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;starting...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# skip header
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error reading file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# ...total by category, flag over LIMIT, print a report...
&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.txt&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;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ...write each category total...
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done, wrote report.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count the landmines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A hardcoded home path&lt;/strong&gt; (&lt;code&gt;/Users/me/Downloads/expenses.csv&lt;/code&gt;) you must edit in source to use the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bare &lt;code&gt;except:&lt;/code&gt;&lt;/strong&gt; that swallows &lt;em&gt;every&lt;/em&gt; error — including bugs in your own code — and then keeps running on an empty &lt;code&gt;rows&lt;/code&gt; list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Positional column access&lt;/strong&gt; (&lt;code&gt;row[1]&lt;/code&gt;, &lt;code&gt;row[2]&lt;/code&gt;) that explodes on a missing column and gives no clue which one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;float(row[2])&lt;/code&gt;&lt;/strong&gt; that crashes the whole run on one malformed cell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;print()&lt;/code&gt; for everything&lt;/strong&gt; — diagnostics, results, and the "FLAGGED" lines all jammed into stdout together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A non-atomic write&lt;/strong&gt; (&lt;code&gt;out = open("report.txt", "w")&lt;/code&gt;) that leaves a corrupt file if the program dies mid-loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No tests, no install path, no &lt;code&gt;--help&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. This is what &lt;em&gt;working&lt;/em&gt; AI-generated Python looks like. Let's ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist (run this on any script)
&lt;/h2&gt;

&lt;p&gt;This is the rubric I run before code goes to other people. Score each 0–2, twenty points max; be strict, "partially" is a 1. Each item is a &lt;strong&gt;Failure → Fix&lt;/strong&gt; pair so you can skim it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; a bare &lt;code&gt;except:&lt;/code&gt; (automatic 0), or raw tracebacks on bad input. &lt;em&gt;Fix:&lt;/em&gt; wrap I/O, parsing, and network calls in &lt;em&gt;specific&lt;/em&gt; exceptions; failures produce actionable messages and a nonzero exit code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets &amp;amp; config&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; hardcoded keys, tokens, or home paths. &lt;em&gt;Fix:&lt;/em&gt; config comes from arguments or env. Grep your own code for &lt;code&gt;api_key =&lt;/code&gt;, &lt;code&gt;password =&lt;/code&gt;, &lt;code&gt;token =&lt;/code&gt; before you ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs &amp;amp; validation&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; the script assumes well-formed input. &lt;em&gt;Fix:&lt;/em&gt; check every external input — empty file? missing column? a path with spaces?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging &amp;amp; observability&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; &lt;code&gt;print()&lt;/code&gt; for diagnostics, or total silence on failure. &lt;em&gt;Fix:&lt;/em&gt; &lt;code&gt;logging&lt;/code&gt; with levels; user output separated from debug noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; none (most scripts). &lt;em&gt;Fix:&lt;/em&gt; a pytest suite covering the happy path &lt;em&gt;and&lt;/em&gt; at least three failure modes, running green.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency hygiene&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; undeclared or unpinned deps, dead imports. &lt;em&gt;Fix:&lt;/em&gt; declare dependencies with version bounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface &amp;amp; UX&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; values you edit in source to run it. &lt;em&gt;Fix:&lt;/em&gt; a real CLI (&lt;code&gt;--help&lt;/code&gt;, exit codes) or a documented API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packaging &amp;amp; install&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; "clone it and run &lt;code&gt;python script.py&lt;/code&gt; and hope." &lt;em&gt;Fix:&lt;/em&gt; &lt;code&gt;pip install .&lt;/code&gt; works, an entry point is defined, it runs from any directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; no runnable example. &lt;em&gt;Fix:&lt;/em&gt; a README with one-line purpose, install, a copy-pasteable example, and expected output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt; — &lt;em&gt;Failure:&lt;/em&gt; &lt;code&gt;open()&lt;/code&gt; without &lt;code&gt;encoding=&lt;/code&gt; blows up on a non-UTF-8 file. &lt;em&gt;Fix:&lt;/em&gt; always pass &lt;code&gt;encoding="utf-8"&lt;/code&gt;; state the Python version; verify from a fresh venv.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one rule that makes this checklist actually useful: &lt;strong&gt;every finding must cite a file and line.&lt;/strong&gt; "Improve error handling" is banned. "&lt;code&gt;open()&lt;/code&gt; at line 14 crashes on a missing file" is the standard.&lt;/p&gt;

&lt;p&gt;Now the three fixes that matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: bare &lt;code&gt;except&lt;/code&gt; → specific exceptions + real exit codes
&lt;/h2&gt;

&lt;p&gt;The doctrine is short: &lt;strong&gt;catch specific, never broad; catch at the edge, not in the middle.&lt;/strong&gt; Inner code raises; it doesn't apologize. Only &lt;code&gt;main()&lt;/code&gt; translates an error into a message and an exit code — 0 success, 1 runtime failure, 2 usage error — or you break every shell pipeline and cron job built on the script.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Expected, user-facing failure. Message says what to do, not just what broke.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;REQUIRED_COLUMNS&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;date&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;category&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;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_expenses&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="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;is_file&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input file not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REQUIRED_COLUMNS&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fieldnames&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[])]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is missing required column(s): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&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="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crucially, &lt;code&gt;main()&lt;/code&gt; catches &lt;code&gt;AppError&lt;/code&gt; only. An &lt;em&gt;unexpected&lt;/em&gt; exception still surfaces loudly with its traceback — because a swallowed crash is undiagnosable, while a loud one tells you exactly what to fix. The output write also became atomic — write to a &lt;code&gt;.tmp&lt;/code&gt; file, then rename it into place — so a crash never leaves a corrupt report behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: &lt;code&gt;print()&lt;/code&gt; → logging without breaking your pipes
&lt;/h2&gt;

&lt;p&gt;The distinction nobody gets right: &lt;strong&gt;program output is not logging.&lt;/strong&gt; If the script's job is to print a report to stdout, that stays &lt;code&gt;print()&lt;/code&gt;. Logging is the diagnostic narration around it, and it goes to &lt;strong&gt;stderr&lt;/strong&gt;. Mix them and you break &lt;code&gt;script.py &amp;gt; out.csv&lt;/code&gt; for every user.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_logging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Level discipline: per-row detail is &lt;code&gt;DEBUG&lt;/code&gt;; "wrote report.csv" is &lt;code&gt;INFO&lt;/code&gt;; "skipped 3 rows" is &lt;code&gt;WARNING&lt;/code&gt;. Use the lazy &lt;code&gt;%s&lt;/code&gt; form so formatting is skipped when the level is off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: loose script → installable, tested CLI
&lt;/h2&gt;

&lt;p&gt;The hardcoded &lt;code&gt;INPUT&lt;/code&gt; path became an &lt;code&gt;argparse&lt;/code&gt; CLI with &lt;code&gt;--help&lt;/code&gt;, &lt;code&gt;--limit&lt;/code&gt;, &lt;code&gt;--output&lt;/code&gt;, &lt;code&gt;--force&lt;/code&gt;, &lt;code&gt;-v&lt;/code&gt;, and &lt;code&gt;--version&lt;/code&gt;. The loose file became a &lt;code&gt;src/&lt;/code&gt; layout package with a &lt;code&gt;pyproject.toml&lt;/code&gt; entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project.scripts]&lt;/span&gt;
&lt;span class="py"&gt;expense-report&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"expense_report.cli:main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what turns "clone it and run &lt;code&gt;python script.py&lt;/code&gt; and hope" into a real command. I verified the whole chain in a fresh virtual environment before writing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; .venv/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
.venv/bin/python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest        &lt;span class="c"&gt;# 16 passed&lt;/span&gt;
.venv/bin/expense-report &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All sixteen tests passing, clean install, runs from any directory. Evidence over confidence — that's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on your own code
&lt;/h2&gt;

&lt;p&gt;That checklist is yours; the worked example above is the whole method. Audit one of your "finished" scripts against the ten categories, fix the blocking issues first, and prove each fix with a failure you can actually trigger.&lt;/p&gt;

&lt;p&gt;If you'd rather your AI assistant &lt;em&gt;enforce&lt;/em&gt; this loop instead of doing it by hand, I packaged the discipline as eight Claude Code skills — the scored audit (&lt;code&gt;ship-check&lt;/code&gt;), plus &lt;code&gt;harden-errors&lt;/code&gt;, &lt;code&gt;add-logging&lt;/code&gt;, &lt;code&gt;make-cli&lt;/code&gt;, &lt;code&gt;add-tests&lt;/code&gt;, &lt;code&gt;package-it&lt;/code&gt;, &lt;code&gt;write-readme&lt;/code&gt;, and a &lt;code&gt;release-prep&lt;/code&gt; gate — with the full before/after sample project. Full disclosure: I built it, and it's a paid kit ($19) at &lt;a href="https://jackiecole.gumroad.com/l/lcscdf" rel="noopener noreferrer"&gt;jackiecole.gumroad.com/l/lcscdf&lt;/a&gt;. The checklist in this post stands on its own either way.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Shipped One Messy Python Script. Here's the 10-Point Checklist That Got It There.</title>
      <dc:creator>2-glitch</dc:creator>
      <pubDate>Sat, 13 Jun 2026 11:21:55 +0000</pubDate>
      <link>https://dev.to/2glitch/i-shipped-one-messy-python-script-heres-the-10-point-checklist-that-got-it-there-58jo</link>
      <guid>https://dev.to/2glitch/i-shipped-one-messy-python-script-heres-the-10-point-checklist-that-got-it-there-58jo</guid>
      <description>&lt;p&gt;AI can generate a functional Python script in approximately ninety seconds. The script executes successfully, and the user proceeds to the next task.&lt;/p&gt;

&lt;p&gt;However, the script often persists beyond its initial use. It may include a hard-coded Downloads path or a comment such as # edit before running!!. Subsequently, someone might duplicate it as script_v2_FINAL.py. Weeks later, a teammate may attempt to use the script with a slightly different CSV file, resulting in a raw traceback or, more problematically, the script silently producing an incomplete file and exiting with a success code.&lt;/p&gt;

&lt;p&gt;That gap between "runs on my machine" and "I'd let someone else run this" — that distance is the actual engineering work. And AI almost never closes it for you, because you never asked. (That's an observation from watching a lot of generated coThis post provides a detailed analysis of a deliberately typical, disorganized script. I will identify each defect and demonstrate the necessary modifications to prepare it for distribution. By the conclusion, readers will have a ten-point checklist applicable to their own code, requiring only Python and pytest.on your own code today, with zero tools beyond Python and pytest.&lt;/p&gt;

&lt;p&gt;The "before" — a script that works exactly once&lt;br&gt;
The following is the initial script. It aggregates expenses from a CSV file by category and flags entries exceeding a specified limit. Although it functions as intended, this apparent success can be misleading. (The script is abbreviated for brevity; the overall structure remains representative.)&lt;/p&gt;

&lt;h1&gt;
  
  
  expense report script v2 FINAL (working!!)
&lt;/h1&gt;

&lt;p&gt;import csv&lt;/p&gt;

&lt;p&gt;INPUT = "/Users/me/Downloads/expenses.csv"   # &amp;lt;- edit this before running!!&lt;br&gt;
LIMIT = 500&lt;/p&gt;

&lt;p&gt;print("starting...")&lt;/p&gt;

&lt;p&gt;rows = []&lt;br&gt;
try:&lt;br&gt;
    f = open(INPUT)&lt;br&gt;
    r = csv.reader(f)&lt;br&gt;
    next(r)  # skip header&lt;br&gt;
    for row in r:&lt;br&gt;
        rows.append(row)&lt;br&gt;
except:&lt;br&gt;
    print("error reading file")&lt;/p&gt;

&lt;p&gt;for row in rows:&lt;br&gt;
    cat = row[1]&lt;br&gt;
    amt = float(row[2])&lt;br&gt;
    # ...total by category, flag over LIMIT, print a report...&lt;/p&gt;

&lt;p&gt;out = open("report.txt", "w")&lt;/p&gt;

&lt;h1&gt;
  
  
  ...write each category Identify the critical issues present in the script:, wrote report.txt")
&lt;/h1&gt;

&lt;p&gt;Count the landmines:&lt;/p&gt;

&lt;p&gt;A hardcoded home path (/Users/me/Downloads/expenses.csv) you must edit in source to use the tool.&lt;br&gt;
A bare except: that swallows every error — including bugs in your own code — and then keeps running on an empty rows list.&lt;br&gt;
Positional column access (row[1], row[2]) that explodes on a missing column and gives no clue which one.&lt;br&gt;
float(row[2]) that crashes the whole run on one malformed cell.&lt;br&gt;
print() for everything — diagnostics, results, and the "FLAGGED" lines all jammed into stdout together.&lt;br&gt;
A non-atomic write (out = open("report.txt", "These issues are common and representative of typical AI-generated Python scripts. The following sections will address how to prepare such scripts for reliable distribution. --help.&lt;br&gt;
None of this is exotic. This is what working AI-generated Python looks like. Let's ship it.&lt;/p&gt;

&lt;p&gt;The checklist (run this on any script)&lt;br&gt;
This is the evaluation rubric I apply before sharing code with others. Each criterion is scored from 0 to 2, for a maximum of twenty points. Apply strict standards; a 'partially' addressed item receives a score of 1. Each entry presents a Failure and corresponding Fix for efficient review.&lt;/p&gt;

&lt;p&gt;Error handling — Failure: a bare except: (automatic 0), or raw tracebacks on bad input. Fix: wrap I/O, parsing, and network calls in specific exceptions; failures produce actionable messages and a nonzero exit code.&lt;br&gt;
Secrets &amp;amp; config — Failure: hardcoded keys, tokens, or home paths. Fix: config comes from arguments or env. Grep your own code for api_key =, password =, token =, sk-, Bearer, and absolute home paths before you ship.&lt;br&gt;
Inputs &amp;amp; validation — Failure: the script assumes well-formed input. Fix: check every external input — what happens on an empty file? A missing column? a path with spaces?&lt;br&gt;
Logging &amp;amp; observability — Failure: print() for diagnostics, or total silence on failure. Fix: logging with levels; user output separated from debug noise.&lt;br&gt;
Tests — Failure: none (most scripts). Fix: a pytest suite covering the happy path and at least three failure modes, with all tests running green.&lt;br&gt;
Dependency hygiene — Failure: undeclared or unpinned deps, dead imports. Fix: declare dependencies with version bounds; imports match declared deps.&lt;br&gt;
Interface &amp;amp; UX — Failure: values you edit in source to run it. Fix: a real CLI (--help, exit codes) or a documented API.&lt;br&gt;
Packaging &amp;amp; install — Failure: "clone it and run python script.py and hope." Fix: pip install . works, an entry point is defined, it runs from any directory.&lt;br&gt;
Documentation — Failure: no runnable example. Fix: a README with one-line purpose, install, a copy-pasteable example, and expected output.&lt;br&gt;
Portability — Failure: open() without encoding= blows up on a non-UTF-8 file; OS-specific paths wiA critical rule for effective use of this checklist is that every finding must reference a specific file and line number. General statements such as 'improve error handling' are insufficient. Instead, precise observations like 'open() at line 14 crashes on a missing file' are required. Ambiguous findings are unlikely to be addressed.dling" is banned. "open() at line 14 crashes on a missing file" is the standard. Vague findings never get fixed.&lt;/p&gt;

&lt;p&gt;Now the three fixes that matter most.&lt;/p&gt;

&lt;p&gt;Fix 1: bare except → specific exceptions + real exit codes&lt;br&gt;
The guiding principle is concise: always catch specific exceptions rather than broad ones, and handle exceptions at the program's boundaries rather than within core logic. Internal code should raise exceptions without handling them directly. Only the main() function should convert errors into user-facing messages and exit codes. Exit codes must be meaningful: 0 for success, 1 for runtime failure, and 2 for usage errors. Otherwise, shell pipelines and scheduled jobs that depend on the script may fail unpredictably.&lt;/p&gt;

&lt;p&gt;Here's the validation from the shipped core.py. Notice it names the missing column instead of dying on row[1]:&lt;/p&gt;

&lt;p&gt;class AppError(Exception):&lt;br&gt;
    """ Expected, user-facing failure. Message says what to do, not just what broke."""&lt;/p&gt;

&lt;p&gt;REQUIRED_COLUMNS = ("date", "category", "amount")&lt;/p&gt;

&lt;p&gt;def read_expenses(path: Path) -&amp;gt; list[dict[str, str]]:&lt;br&gt;
    if not path.is_file():&lt;br&gt;
        raise AppError(f"input file not found: {path}")&lt;br&gt;
    with open(path, newline="", encoding="utf-8") as f:&lt;br&gt;
        reader = csv.DictReader(f)&lt;br&gt;
        if reader. fieldnames is None:&lt;br&gt;
            raise AppError(f"{path} is empty — expected a header row")&lt;br&gt;
        missing = [c for c in REQUIRED_COLUMNS if c not in reader.fieldnames]&lt;br&gt;
        if missing:&lt;br&gt;
            raise AppError(f"{path} is missing required column(s): {', '.join(missing)}")&lt;br&gt;
        return list(reader)&lt;br&gt;
A bad row no longer kills the run — it's skipped with a warning and counted. And the top of main() does the translating:&lt;/p&gt;

&lt;p&gt;try:&lt;br&gt;
    rows = read_expenseImportantly, this approach catches only AppError exceptions. Unexpected exceptions are allowed to propagate, displaying a full traceback. Suppressing such errors makes diagnosis difficult, whereas explicit tracebacks provide actionable information. Avoid wrapping main() in a generic exception handler that outputs only a vague error message.aceback — because a swallowed crash is undiagnosable, while a loud one tells you exactly what to fix. Don't wrap main() in a catch-all that prints "something went wrong."&lt;/p&gt;

&lt;p&gt;That output write also became atomic — write to a .tmp file, then rename it into place — so a crash never leaves a cA common misconception is to conflate program output with logging. If the script is intended to print a report to standard output, use print() for that purpose. Logging, which provides diagnostic information, should be directed to standard error. Mixing these streams can disrupt output redirection for users.t goes to stderr. Mix them, then run script.py &amp;gt; out.csv for every user.&lt;/p&gt;

&lt;p&gt;So the "starting..." and "read N rows" lines became leveled logs to stderr, wired to a verbosity flag — quiet by default, -v for milestones, -vv for debug:&lt;/p&gt;

&lt;p&gt;def setup_logging(verbosity: int = 0) -&amp;gt; None:&lt;br&gt;
    level = [logging.WARNING, logging.INFO, logging.DEBUG][min(verbosity, 2)]&lt;br&gt;
    logging.basicConfig(level=level, handlers=[logging.StreamHandler(sys.stderr)])&lt;br&gt;
Maintain discipline in logging levels: detailed information for each row should be logged at the DEBUG level; summary actions such as 'wrote report.csv' should use INFO; and warnings like 'skipped 3 rows with missing amount' should use WARNING. INFO-level messages should occur only a constant number of times per execution, never within per-row loops. Additionally, use the lazy formatting style (log.debug("row %d amount=%s", i, amt)) to avoid unnecessary string formatting when the log level is not active.&lt;/p&gt;

&lt;p&gt;Fix 3: loose script → installable, tested CLI&lt;br&gt;
The hardcoded INPUT path was replaced by an argparse CLI with --help, --limit, --output, --force, -v, and --version. The loose file became a src/ layout package with a pyproject. toml declaring an entry point:&lt;/p&gt;

&lt;p&gt;[project.scripts]&lt;br&gt;
expense-report = "expense_report.cli:main"&lt;br&gt;
That's what turns "clone it and run python script.py and hope" into a real command. And the pytest suite covers the happy path plus the failure modes the original couldn't survive: missing file, empty file, missing column, non-numeric amount (skipped, not fatal), and refusing to overwrite an existing report without --force.&lt;/p&gt;

&lt;p&gt;I verified the whole chain in a fresh virtual environment before writing this:&lt;/p&gt;

&lt;p&gt;python3 -m venv .venv &amp;amp;&amp;amp; .venv/bin/pip install ".[dev]"&lt;br&gt;
.venv/bin/python -m pytest        # 16 passed&lt;br&gt;
.venv/bin/expense-report --help&lt;br&gt;
All sixteen tests pass, the installation process is clean, and the script executes correctly from any directory. Demonstrating evidence of reliability. The checklist provided is intended for your use, and the preceding example demonstrates the complete methodology. Evaluate one of your completed scripts against the ten categories, address the most critical issues first, and verify each fix by reproducing the corresponding failure. The determination of whether a script 'needs work' is subjective, but scripts with low scores in error handling, testing, and packaging should not be distributed. A low score in error handling, tests, and packaging is one you shouldn't hand to anyone yet.&lt;/p&gt;

&lt;p&gt;If you'd rather your AI assistant enforce this loop instead of doing it by hand, I packaged the discipline as eight Claude Code skills — the scored audit (ship-check), plus harden-errors, add-logging, make-cli, add-tests, package-it, write-readme, and a release-prep gate — along with the full before/after sample project you just read through. Full disclosure: I built it, and it's a paid kit ($19) at jackiecole.gumroad.com/l/lcscdf. The checklist in this post stands on its own either way.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
