<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Notes</title>
<link>https://blog.targeting.ai/</link>
<atom:link href="https://blog.targeting.ai/index.xml" rel="self" type="application/rss+xml"/>
<description>Essays on the methods behind the targeting.ai apps.</description>
<generator>quarto-1.8.27</generator>
<lastBuildDate>Sat, 08 Aug 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>The happy path is a lie</title>
  <link>https://blog.targeting.ai/posts/the-happy-path-is-a-lie/</link>
  <description><![CDATA[ 





<p>For about seven months a small nightly job did exactly what I asked of it. It read the day’s threads from a public discussion forum, kept the six fields I cared about, ranked them by likes, and wrote the top five into a table that a dashboard picked up the next morning. It never paged me, so I stopped reading its logs.</p>
<p>Then one Tuesday the dashboard showed a quiet week. Activity down, three days running. I am a sociologist by training, so I did what I am trained to do: I opened a notebook and started drafting a paragraph about seasonal decline in forum engagement.</p>
<p>There was no decline. The forum had reorganised its API. The endpoint still answered, still returned valid JSON, and no longer carried the key my parser wanted. My code caught the <code>KeyError</code>, wrote one line into a log nobody reads, returned <code>[]</code>, and exited zero. Three nights in a row it had overwritten real data with nothing — on schedule, successfully.</p>
<p>I spent that morning treating a measurement artifact as a social fact. The pipeline had given me no way to tell the two apart, because an empty result and a quiet week are the same object. That was true of every pipeline I wrote in 2025, and I wrote a lot of them.</p>
<section id="four-failures-wearing-one-coat" class="level2">
<h2 class="anchored" data-anchor-id="four-failures-wearing-one-coat">Four failures wearing one coat</h2>
<p>The bug was not the <code>KeyError</code>. The bug was the shape of the function around it:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> scrape(source: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>[<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">dict</span>]:</span>
<span id="cb1-2">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">try</span>:</span>
<span id="cb1-3">        ...</span>
<span id="cb1-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">except</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Exception</span> <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> error:</span>
<span id="cb1-5">        log.warning(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"scrape failed: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%s</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>, error)</span>
<span id="cb1-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> []</span></code></pre></div></div>
<p>That signature promises <code>list[dict]</code> and the body underneath it can fail four unrelated ways. The network can be down, so nothing arrives. The host can be up and answer 500. Bytes can arrive that are not parseable JSON. The JSON can parse cleanly and not have the shape I assumed.</p>
<p>Those are four different facts about the world, and they want four different responses. Wait and try again. Back off and try again much later. Try again right now, because a body that stops mid-object is almost always transport. Stop entirely, go read the upstream changelog, and change the code — because the contract moved and no amount of retrying will put the key back.</p>
<p><code>except Exception</code> erases all four distinctions at the one moment they still exist. After that line the caller holds an empty list, and the list does not know why it is empty. Nothing downstream can recover what was thrown away upstream. That is the missing branch: not a line I forgot to write, but a difference I declined to carry.</p>
</section>
<section id="giving-the-failures-names" class="level2">
<h2 class="anchored" data-anchor-id="giving-the-failures-names">Giving the failures names</h2>
<p>The fix is boring, which is the best thing about it. Four frozen dataclasses and one <code>match</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@dataclass</span>(frozen<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb2-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> NetworkError:</span>
<span id="cb2-3">    cause: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span></span>
<span id="cb2-4"></span>
<span id="cb2-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@dataclass</span>(frozen<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb2-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> ApiError:</span>
<span id="cb2-7">    status: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span></span>
<span id="cb2-8"></span>
<span id="cb2-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@dataclass</span>(frozen<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb2-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> ParseError:</span>
<span id="cb2-11">    detail: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span></span>
<span id="cb2-12"></span>
<span id="cb2-13"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@dataclass</span>(frozen<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb2-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> ShapeError:</span>
<span id="cb2-15">    detail: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span></span>
<span id="cb2-16"></span>
<span id="cb2-17">ScrapeError <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> NetworkError <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> ApiError <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> ParseError <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> ShapeError</span>
<span id="cb2-18"></span>
<span id="cb2-19"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> classify(error: <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Exception</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> ScrapeError:</span>
<span id="cb2-20">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">match</span> error:</span>
<span id="cb2-21">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> httpx.HTTPStatusError():</span>
<span id="cb2-22">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> ApiError(status<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>error.response.status_code)</span>
<span id="cb2-23">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> httpx.HTTPError() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">OSError</span>():</span>
<span id="cb2-24">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> NetworkError(cause<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(error))</span>
<span id="cb2-25">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> json.JSONDecodeError():</span>
<span id="cb2-26">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> ParseError(detail<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(error))</span>
<span id="cb2-27">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">KeyError</span>() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">TypeError</span>() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">ValueError</span>():</span>
<span id="cb2-28">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> ShapeError(detail<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">repr</span>(error))</span>
<span id="cb2-29">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">case</span> _:</span>
<span id="cb2-30">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> NetworkError(cause<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">repr</span>(error))</span></code></pre></div></div>
<p>Each name carries a payload, because a label without evidence is not much better than a stack trace. <code>ApiError(status=503)</code> and <code>ApiError(status=404)</code> deserve different treatment, and the caller is the one who should decide which.</p>
<p><code>classify</code> runs once, at the edge, as the last step of the pipeline:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> scrape(source: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>, top_n: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> Result[<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>[<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">dict</span>], ScrapeError]:</span>
<span id="cb3-2">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (</span>
<span id="cb3-3">        read_source(source)</span>
<span id="cb3-4">        .bind(parse_json)</span>
<span id="cb3-5">        .bind(extract_topics)</span>
<span id="cb3-6">        .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">map</span>(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> ts: [clean_topic(t) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> t <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> ts])</span>
<span id="cb3-7">        .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">map</span>(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> ts: rank_topics(ts, n<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>top_n))</span>
<span id="cb3-8">        .alt(classify)</span>
<span id="cb3-9">    )</span></code></pre></div></div>
<p>The library here is <code>returns</code>, which gives me <code>Result</code>, <code>Success</code>, <code>Failure</code>, and <code>.bind</code>. That part is replaceable; a hand-rolled tagged union would do the same work. What is not replaceable is the signature. It now says out loud that this function has two outcomes, and it names the second one.</p>
</section>
<section id="both-versions-run-on-the-same-three-files" class="level2">
<h2 class="anchored" data-anchor-id="both-versions-run-on-the-same-three-files">Both versions, run on the same three files</h2>
<div id="a68907bd" class="cell" data-execution_count="1">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> sys</span>
<span id="cb4-2">sys.path.insert(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"."</span>)</span>
<span id="cb4-3"></span>
<span id="cb4-4"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> railway.pipeline <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> scrape</span>
<span id="cb4-5"></span>
<span id="cb4-6"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> fixture <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"topics.json"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"broken.json"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"wrong-shape.json"</span>):</span>
<span id="cb4-7">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>fixture<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:20}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>scrape(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'fixtures/</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>fixture<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span>, top_n<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>topics.json          &lt;Success: [{'posts_count': 8, 'views': 5100, 'like_count': 88, 'id': 2, 'title': 'Why our nightly ETL lies about success', 'created_at': '2026-06-11T17:02:00Z'}, {'posts_count': 12, 'views': 3400, 'like_count': 41, 'id': 1, 'title': 'Retry storms in a queue worker', 'created_at': '2026-06-02T09:14:00Z'}]&gt;
broken.json          &lt;Failure: ParseError(detail='Expecting value: line 2 column 1 (char 28)')&gt;
wrong-shape.json     &lt;Failure: ShapeError(detail="KeyError('topic_list')")&gt;</code></pre>
</div>
</div>
<p><em>Figure 1 — frozen at render time, 8 Aug 2026</em></p>
<p>One call site, three inputs, three labelled outcomes. <code>topics.json</code> is a normal response and comes back as <code>Success</code> with the two highest-liked threads. <code>broken.json</code> is a body that stopped mid-write; it comes back as <code>ParseError</code> carrying the character offset where the parser gave up, and retrying it is reasonable, because that is usually a connection that died. <code>wrong-shape.json</code> is valid JSON without a <code>topic_list</code>; it comes back as <code>ShapeError</code>, and that is my Tuesday morning. Retrying it produces the same <code>KeyError</code> forever. Something upstream changed and a human has to look.</p>
<p><code>try/except</code> can catch both of those. What it cannot do is hand the difference to the caller as a value.</p>
<p>The two names the figure does not show, <code>NetworkError</code> and <code>ApiError</code>, work the same way; they are only harder to stage from a file on disk.</p>
<p>And none of this means <code>try/except</code> is bad. The fetch layer of this same package is a bare <code>try: ... except Exception as error: return Failure(error)</code>. Catching broadly at the boundary and converting the throw into a value is exactly what the construct is for. The failure mode is <code>try/except</code> as the <em>caller’s</em> entire vocabulary for what went wrong.</p>
</section>
<section id="what-it-costs" class="level2">
<h2 class="anchored" data-anchor-id="what-it-costs">What it costs</h2>
<p>Every signature gets longer and stays longer. <code>list[dict]</code> becomes <code>Result[list[dict], ScrapeError]</code>, and every caller has to open the box before it can do anything, including the callers that only ever wanted the happy path. Tests get an extra layer of unwrapping. Notebooks get uglier.</p>
<p><code>.bind</code> chains read strangely for a good while. I still reach for <code>.map</code> where <code>.bind</code> belongs, and the type checker is usually the only reason I notice before the tests do.</p>
<p>The taxonomy has to be agreed before it pays anything, and four categories is a design decision, not a discovery. If two services in the same system disagree about what <code>ShapeError</code> means, you are worse off than with <code>except Exception</code>, because the labels look authoritative and are not.</p>
<p>Mine is not finished either. Look at the last case in <code>classify</code>: an exception it does not recognise is labelled <code>NetworkError</code>. That tells the caller “transient, retry” about a failure nobody has understood yet. It is wrong, it is mine, and the honest fix is a fifth case for the unknown, which I have not written. A taxonomy with a permissive default branch quietly lies at its edges.</p>
<p>And the job still fails exactly as often as it did before. It fails on the same nights, for the same reasons, at the same rate. What changed is that the failure now arrives with a name attached, and the three days I lost to a decline that never happened would have been three lines in a log saying <code>ShapeError</code>.</p>
<p>The full version of this argument — the box, the two tracks, and how you test code shaped like this — is a short book I wrote around this same scraper, <em>The Happy Path Is a Lie</em>.</p>
<p>The code above is vendored verbatim from <a href="https://github.com/nnfuzzy/blog101/tree/main/posts/the-happy-path-is-a-lie/railway/">the railway package</a>, from the book <em>The Happy Path Is a Lie</em>.</p>


</section>

 ]]></description>
  <category>functional</category>
  <category>python</category>
  <guid>https://blog.targeting.ai/posts/the-happy-path-is-a-lie/</guid>
  <pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
