<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://joshcannon.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://joshcannon.me/" rel="alternate" type="text/html" /><updated>2026-04-03T17:53:51+00:00</updated><id>https://joshcannon.me/feed.xml</id><title type="html">A Blog</title><subtitle>A blog. Specifically, Josh Cannon&apos;s Blog.</subtitle><author><name>Josh Cannon</name></author><entry><title type="html">Choices (a poem)</title><link href="https://joshcannon.me/2026/04/03/choices.html" rel="alternate" type="text/html" title="Choices (a poem)" /><published>2026-04-03T00:00:00+00:00</published><updated>2026-04-03T00:00:00+00:00</updated><id>https://joshcannon.me/2026/04/03/choices</id><content type="html" xml:base="https://joshcannon.me/2026/04/03/choices.html"><![CDATA[<p>After I left Anthropic (partially reasoned below), I found I had creativity and hobbies again.
I sprouted some new ones, though, like writing (mostly “silly”) poetry. 
Those who know me know I love wordplay, including rhymes, so this has been a fun activity for me personally
to get to enjoy taking English apart and putting it back together (the silliness comes from doing to English
what a child might do to a Mr. Potato Head doll).</p>

<p>The following is a poem, but not a silly one, which I’m using to help me justify (to myself and others) my choices:
past, present, and hopefully future. Enjoy!</p>

<blockquote>
  <p>Some people stand on the shoulders of giants,</p>

  <p>while others go run with the bulls.</p>

  <p>I’ve seen if I tried I could stand and could run,</p>

  <p>but it leaves my glass only half full.</p>

  <p>-</p>

  <p>Well, that’s not quite true, see, it’s filled to the brim.</p>

  <p>‘Cuz my glass truly doth runneth over.</p>

  <p>But only half full with the blood of my being,</p>

  <p>while the other half’s empty with water.</p>

  <p>-</p>

  <p>I think I’ll gulp less from the Fountain of Knowledge,</p>

  <p>the beverage which sates not the soul.</p>

  <p>And hope that the liquid which flows from my heart,</p>

  <p>will fill cups before I’m too old.</p>
</blockquote>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[After I left Anthropic (partially reasoned below), I found I had creativity and hobbies again. I sprouted some new ones, though, like writing (mostly “silly”) poetry. Those who know me know I love wordplay, including rhymes, so this has been a fun activity for me personally to get to enjoy taking English apart and putting it back together (the silliness comes from doing to English what a child might do to a Mr. Potato Head doll).]]></summary></entry><entry><title type="html">I’m spoiled without toil</title><link href="https://joshcannon.me/2025/12/26/toil.html" rel="alternate" type="text/html" title="I’m spoiled without toil" /><published>2025-12-26T00:00:00+00:00</published><updated>2025-12-26T00:00:00+00:00</updated><id>https://joshcannon.me/2025/12/26/toil</id><content type="html" xml:base="https://joshcannon.me/2025/12/26/toil.html"><![CDATA[<p>I’m excited for what the future has to offer us.</p>

<p>Technology brings so much excitement and innovation and, most-importantly
brings us closer to being our most human selves, free from toil.</p>

<p>I’ve been reflecting on all the wonderful advances in toil-reducing automation
I now realize I’ve come to assume as commonplace.
As an exercise in gratitude, I thought I’d list them.</p>

<hr />

<p>Thanks to personal automobiles, humans have been freed from the toil involved
in short and medium distance travel. Although, this was true when the automobile became
a household accessory, it became true yet again when the technology advanced to a point
where you could travel in a similiarly-sized autombile at twice the speed (which also
had the added benefit of removing the toil of a slow and prolonged death for a great
multitude of people).</p>

<p>For long-distance travel, look no farther than your local airport.
The airplane, the legendary FLYING transport of the sky, now offers humanity
a way to travel across continents and oceans without the slightest inkling of toil.
Just close your eyes and imagine how, after a day spent traveling with your family,
you lack the fatigue associated with the toils of long-distance travel of yore.</p>

<p>And speaking of fatigue, let’s be grateful for the technological advancements hiding
in plain sight in our homes. I remember a time, before the advent of the washer and dryer,
when folks would complain about the laundry. But now? We’ve washed our lives clean of toiling
for outwear without a stain or wrinkle.</p>

<p>The (clothes) washer isn’t the only washer which requires praise for innovation. Such toilful
duties “the dishes” used to require, before the advent of the dishwasher. Where there was sweat
and labor and sore muscles before, now jubilent smiles and a song on the lips as “doing the dishes”
has become a synonym for “leisure”.</p>

<p>And speaking of dishes, cooking the food on them also went through a technological revolution.
Not only did the electric stove and oven elminate the toil of making heat from sticks, but
again the toil (which was already vanquished) was eliminated when mankind introduced: the microwave.
Gone was the struggles of slowly and painstakingly exherting yourself over a hot stove or oven.
The microwave as a technological advncement can be thanked for having a hot, nutrious, sating meal
every meal.</p>

<hr />

<p>All the technological advancements (and so many more) sum up to a life spent in bliss. The free time
I enjoy because of them I’ve been able to trade for currency which unlocks EVEN MORE toil-expelling
technologies.</p>

<p>My life simply couldn’t get any easier or more relaxing, and yet somehow it will with tomorrow’s
“latest and greatest” innovation in automation.</p>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[What's more human than technology?]]></summary></entry><entry><title type="html">Hubris</title><link href="https://joshcannon.me/2025/10/28/hubris.html" rel="alternate" type="text/html" title="Hubris" /><published>2025-10-28T00:00:00+00:00</published><updated>2025-10-28T00:00:00+00:00</updated><id>https://joshcannon.me/2025/10/28/hubris</id><content type="html" xml:base="https://joshcannon.me/2025/10/28/hubris.html"><![CDATA[<h1 id="exhibit-a">Exhibit A</h1>

<p>After I announced I was leaving Anthropic I sent this meme to my wife and a friend:</p>

<p><img src="https://github.com/user-attachments/assets/1988e05e-90aa-4cb2-986a-0b9e0d239791" alt="meme" /></p>

<h1 id="exhibit-b">Exhibit B</h1>

<blockquote>
  <p>Thank you for taking the time to interview with &lt;Human&gt;, &lt;Human&gt;, and &lt;Human&gt;.
I’ve read their feedback and based on our discussion about salary expectations,
which fall into the range of Principal Engineer at &lt;Company&gt;,
I’ve decided not to move forward with an offer at this time.</p>
</blockquote>

<h1 id="exhibit-c">Exhibit C</h1>

<blockquote>
  <p>Thanks for taking the time to speak with us about opportunities at &lt;Company&gt;.
After reviewing your candidacy with the larger team,
it seems your background isn’t an exact match for the current needs.</p>
</blockquote>

<p>(this one was from me flubbing a surprise leetcode question. Oof)</p>

<h1 id="exhibit-d">Exhibit D</h1>

<blockquote>
  <p>After much deliberation the team decided that it is not a great fit at this time.
Please let me know if you have any questions or feedback.
We highly encourage you to apply to other roles that may appear to be a better fit within &lt;Company&gt;.</p>
</blockquote>

<h1 id="exhibit-e">Exhibit E</h1>

<blockquote>
  <p>After talking as a team, we’ve made the hard decision not to move forward from here.
I understand this is a disappointing outcome, but the reality is that we hold an exceptionally high bar
and say no to a lot of strong candidates — we truly need to be blown away to move forward.</p>
</blockquote>

<h1 id="verdict">Verdict</h1>

<p>It’s good to be confident. I’d make and send the meme again if I had to. But deep down I’m also guilty
of having felt like the meme wasn’t a joke. This time I lucked out:</p>

<blockquote>
  <p>Do you have a second to connect today? I’ve got some feedback for you :)</p>
</blockquote>

<p>Next time (oh I REALLY hope there won’t be a next time), I’ll work hard and avoid getting any desserts.</p>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[Exhibit A]]></summary></entry><entry><title type="html">Making executable markdown files</title><link href="https://joshcannon.me/2025/10/22/executable-markdown.html" rel="alternate" type="text/html" title="Making executable markdown files" /><published>2025-10-22T00:00:00+00:00</published><updated>2025-10-22T00:00:00+00:00</updated><id>https://joshcannon.me/2025/10/22/executable-markdown</id><content type="html" xml:base="https://joshcannon.me/2025/10/22/executable-markdown.html"><![CDATA[<p>In the pursuit of something itself worthy of a (future) blog post,
I foud myself wanting to have “exeuctable” markdown files.</p>

<p>TL;DR <code class="language-plaintext highlighter-rouge">[ \]; exec &lt;COMMAND&gt; $0 "$@"; ]:#</code></p>

<p>Try it! <code class="language-plaintext highlighter-rouge">curl https://raw.githubusercontent.com/thejcannon/joshcannon.me/refs/heads/main/scripts/magic-word.md | sh</code></p>

<p>(or save it to a file, <code class="language-plaintext highlighter-rouge">chmod +x</code> it, then run it)</p>

<h1 id="what-the-hell">What the hell?</h1>

<p>Figuring out the puzzle is left as an exercise for the reader, but I’ll present two key pieces of information:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">[ &lt;anything&gt; ]:#</code> is a valid Markdown comment
    <ul>
      <li>You cannot put a space between the <code class="language-plaintext highlighter-rouge">]</code> and the <code class="language-plaintext highlighter-rouge">:</code></li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">[</code> is not Bash syntax, it is a program. (<code class="language-plaintext highlighter-rouge">man [</code> if you don’t believe me)</li>
</ol>

<p>Enjoy!</p>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[In the pursuit of something itself worthy of a (future) blog post, I foud myself wanting to have “exeuctable” markdown files.]]></summary></entry><entry><title type="html">Bounding your problems</title><link href="https://joshcannon.me/2025/10/17/bounding.html" rel="alternate" type="text/html" title="Bounding your problems" /><published>2025-10-17T00:00:00+00:00</published><updated>2025-10-17T00:00:00+00:00</updated><id>https://joshcannon.me/2025/10/17/bounding</id><content type="html" xml:base="https://joshcannon.me/2025/10/17/bounding.html"><![CDATA[<p>Ever since I read this image/meme it has stuck with me:</p>

<p><img width="310" height="500" alt="image" src="https://github.com/user-attachments/assets/acdc79af-2d57-4b69-8332-c0ad49ab2e55" /></p>

<p>Its a game the spare neurons (yes I have a few to spare) like to play while life goes on.
Instead of elevator music playing in my cerebellum, the vast empty silence is masked with silly word games.</p>

<p>I’ve found them to be quite quite useful at times.</p>

<p>Let me demonstrate with a silly example.</p>

<h1 id="a-silly-example">A Silly Example</h1>

<p>There’s a joke that gets told, usually when trying to demonstrate that correlation doesn’t imply causation:</p>

<blockquote>
  <p>100% of people who drink water die.</p>
</blockquote>

<p>Aside from the living people who have drank water and aren’t dead yet
(who are probably statistical anomalies or are maybe just “pre-dead”),
this is a universally true fact.</p>

<p>My retort is usually:</p>

<blockquote>
  <p>100% of people who <em>don’t</em> drink water also die. Almost always quicker.</p>
</blockquote>

<p>Also a universally true fact. Leaving us with two, universally true, almost useless facts…</p>

<p>…unless you’re bored and want to play a game of “find the bounds”.</p>

<h2 id="bounding">Bounding</h2>

<p>If you don’t drink water your lower bound is T+0 (you could, of course die immediately following your questionble decision),
and your upper bound is T+4 days (the average is 3, but this is an upper bound so I’m fine shooting higher.
Jesus was likely <em>much</em> smaller than a temple).</p>

<p>If you choose to continue on with your dihydrogen monoxide addiction, you also enjoy a lower bound of T+0 (oops, down the wrong pipe!).
Your upper bound is literally “the upper bound of a human lifespan” (assuming you’re a human) which is about as generous as an upper bound can be.</p>

<p>In finding our bounds, something has emerged. You are dead either way (drats) but drinking water does seem the better choice.</p>

<p>Which leads us then to question, what are the bounds for people who drink soda? Alcohol? Soylent? Coffee?</p>

<p>Now let’s try this using something I heard at work today.</p>

<h1 id="something-i-heard-at-work-today">Something I heard at work today</h1>

<blockquote>
  <p>We don’t invest enough in the infrastructure that allows us to forge new paths forward.</p>
</blockquote>

<p>This was said earnestly and in the context that implied we ought to have built or be building (more of) this infrastructure.
Let’s take a moment and acknowledge the speaker. They are probably saying this out of frustration that they can’t
forge the new path forward that they would like to, due to the lack of supporting infrastructure.
That sucks - let’s be a good person and tell them we understand their frustrations, then sit in the uncomfortable
silence so they they know we know the difficulty.</p>

<p>(while we’re doing that, there’ll likely be some neurons you aren’t using. Queue elevator music)</p>

<h2 id="bounding-1">Bounding</h2>

<p>It’s easy to imagine a lower bound here: no investment (do not pass Go, do not collect $200). No paths forged - womp womp.
Surprisngly I’d actually argue this bound isn’t a bad place to be on the spectrum. It’s very unambiguous and somewhat final
(“there is no investment here - go elsewhere”).</p>

<p>For the upper bound, we could say “infinite investment”, but let’s be more realistic:
“every time someone wants to forge a new path, the infrasturcure already exists”.</p>

<p>You could argue the upper bound be further along the spectrum (something along the lines of “more infrastructure than you
could ever conceivably need”, but I would argue this isn’t a <em>realistic</em> upper bound.</p>

<p>In finding our bounds, something has emerged. Unless our organization lives at the very tippy tip of the spectrum,
our dilemma is doomed to feel underfunded (drats). This may feel bleak and unhelpful, but let me pose an alternative perspective.</p>

<p>Assume you’re <em>guaranteed</em> to underinvest in infrastructure and therefore miss forging <em>some</em> new paths forward.</p>

<ul>
  <li>What paths might be more or less forgeable today?</li>
  <li>What paths are worth forging, and how much infrastratructure is required?</li>
  <li>What investments in our infrastructure would unlock the most paths?</li>
</ul>

<h1 id="the-point">The point</h1>

<p>By taking a data point, and imagining it on a spectrum with realistic bounds, we’re able to see a “bigger picture”.
This reframing helps turn a potentially unhelpful singular point into an array of choices and tradeoffs. You’re no longer
trying to push or nudge the point this way or that, you’re creatively imaginging the places the point could be and how it might get there.</p>

<p>…or at the very least, your neurons have something fun to do.</p>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[Ever since I read this image/meme it has stuck with me:]]></summary></entry><entry><title type="html">The Programmer’s Dilemma</title><link href="https://joshcannon.me/2025/10/13/monorepos.html" rel="alternate" type="text/html" title="The Programmer’s Dilemma" /><published>2025-10-13T00:00:00+00:00</published><updated>2025-10-13T00:00:00+00:00</updated><id>https://joshcannon.me/2025/10/13/monorepos</id><content type="html" xml:base="https://joshcannon.me/2025/10/13/monorepos.html"><![CDATA[<p>Imagine for a moment you had to get a group of 100 random adult human beings
to perform a task that involves groups of them working together 
(and if you really want to succeed, groups would also need to work with other groups).</p>

<p>Let’s assume these humans are rational (most are!), have a good memory (present company only maybe included),
and understand how other humans generally work (as most do).</p>

<p>Now all you can control is one thing: do you let them hunker down in separate rooms, or bring them all into one?</p>

<p>…this isn’t about “Return to Office.” It’s about monorepos!</p>

<p>And TL;DR: The more you co-locate, the more you co-operate and co-llaborate.</p>

<hr />

<h1 id="aside-a-pedants-repo-rant">(aside) A Pedant’s “Repo” Rant</h1>

<p>A “repository” is defined as “a place, building, or receptacle where things are or may be stored.”</p>

<p>As there’s no comment on size or shape, this implies that my belly button is a lint repository just as much
as Earth is a humans repository.</p>

<p>Or, put another way, not only is the <code class="language-plaintext highlighter-rouge">CPython</code> GitHub <a href="https://github.com/python/cpython">repo</a> a repository,
the <a href="https://github.com/python">Python GitHub org</a> is a repository, as is GitHub (.com) itself.</p>

<p>This is important because when people talk about a “monorepo,” sometimes we aren’t all talking about the
exact same thing. Google’s “monorepo” is not like Meta’s is not like (org I’ve worked at using a GitHub git monorepo).</p>

<h1 id="whats-that-human-doing-in-my-machine">What’s that human doing in my machine?</h1>

<p>I often approach solving common organizational problems in a way that is likely uncomfortable to some:
ignore the technology and focus on the humans. They are the ones writing the code, they are the ones working together,
they are the ones that define success or failure.</p>

<p>This is because I strongly believe that “software engineering” is not just engineering, but is also an artistic exercise
and a social experience (I also believe that driving is a form of dancing, just to go ahead and put that out there).
Perhaps put another way, it’s a <a href="https://en.wikipedia.org/wiki/Game_theory">game</a>.</p>

<p>I recently watched a video talking about the <a href="https://en.wikipedia.org/wiki/Prisoner%27s_dilemma#The_iterated_prisoner's_dilemma">“Iterated Prisoner’s Dilemma”</a>.
In the version I watched, centered around Axelrod’s Tournament, people submitted BASIC programs to compete in a game where
both programs “cooperating” earned them each 5 points, only one cooperating earned that one 3 (and the other 0), and neither 
cooperating earned them both 1. Breaking this down a bit:</p>

<ul>
  <li>If you only ever focus on yourself (don’t choose to cooperate) you’ll earn at most 3 points or as little as 1 point</li>
  <li>The fact that the game is played continuously with the same agents is important. The agents have <em>memory</em> and there’s no end in sight</li>
  <li>Cooperation takes <em>continued trust</em>, but yields the highest outcome for everyone</li>
</ul>

<p>This kind of experimental study is used to help us understand otherwise confounding behaviors in evolved creatures.
We’re quite literally evolved to work together. Which is a good thing, since “working together” is usually a requirement
for a successful organization.</p>

<h1 id="a-simple-thought-experiment">A simple thought experiment</h1>

<p>Let’s take away everything that goes into software development that <em>isn’t</em> interacting with source code control.
After you stop salivating about no longer worrying about CI, JIRA, or Agile, you then take the code from every repo
in your org (within reason) and shove them each in a subdirectory in a new single organization.</p>

<p>…what do you think would happen over time? What kind of culture would emerge?</p>

<h1 id="conclusion">Conclusion</h1>

<p>Coding is a social activity. As evolved, social beings we work best when we’re “all in the same room”
so to speak. That includes our code - one of (but not our only) means of communication.</p>

<p>(And once we decide to cooperate and collaborate, I’m sure we’ll find solutions to the technical problems a monorepo
usually entails. I’m not sure it won’t be Bazel (sorry))</p>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[Imagine for a moment you had to get a group of 100 random adult human beings to perform a task that involves groups of them working together (and if you really want to succeed, groups would also need to work with other groups).]]></summary></entry><entry><title type="html">Overwriting GitHub Required Checks with Buildkite</title><link href="https://joshcannon.me/2025/10/01/bk-security.html" rel="alternate" type="text/html" title="Overwriting GitHub Required Checks with Buildkite" /><published>2025-10-01T00:00:00+00:00</published><updated>2025-10-01T00:00:00+00:00</updated><id>https://joshcannon.me/2025/10/01/bk-security</id><content type="html" xml:base="https://joshcannon.me/2025/10/01/bk-security.html"><![CDATA[<p>(for some pre-reading you may want to read <a href="https://joshcannon.me/2025/08/24/github-commit-status.html">my brief post</a>
 on how GitHub commit statuses and PR required checks work)</p>

<h2 id="buildkite-step-level-notify">Buildkite step-level <code class="language-plaintext highlighter-rouge">notify</code></h2>

<p>Buildkite pipelines offer a <a href="https://buildkite.com/docs/pipelines/configure/notifications">nice feature</a>
where the pipeline YAML can have a <code class="language-plaintext highlighter-rouge">notify</code> key which can be configured
to list different services to receive “notifications” about the pipeline’s status
(e.g. “in progress” or “failed” or “succeeded”).</p>

<p>These services include Slack (to send messages/updates) and PagerDuty (presumably to alert on failure) and also GitHub,
although not documented on the linked page - for that you’d need to go <a href="https://buildkite.com/docs/pipelines/source-control/github#customizing-commit-statuses-build-level">here</a>.</p>

<p>This feature is also extended to individual pipeline steps, as well as group steps, with a subset of the configurable services.</p>

<p>As an example:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">steps</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">label</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Example</span><span class="nv"> </span><span class="s">Script"</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">script.sh"</span>
    <span class="na">notify</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">...</span>
</code></pre></div></div>

<h3 id="github_commit_status"><code class="language-plaintext highlighter-rouge">github_commit_status</code></h3>

<p>One of the configurable services services is <code class="language-plaintext highlighter-rouge">github_commit_status</code>,
which takes an arbitrary string <em>context</em>, and tells Buildkite to send commit statuses
(e.g. “pending”, “failure”) representing the pipeline step status (“running”, “failed”) using the provided context.</p>

<p>It hopefully isn’t hard to imagine how this can be a useful feature!
Unfortunately it also isn’t hard to imagine how this can be abused.</p>

<h2 id="setting-up-the-board">Setting up the board</h2>

<ol>
  <li>Configure your repository to require Pull Requests, and require that PRs MUST pass a required status check
(which for this purpose, let’s assume does some security checks) which MUST come from the Buildkite GitHub App
using the context <code class="language-plaintext highlighter-rouge">buildkite/security-pipeline</code> (the one sent by the “Security Pipeline” pipeline).</li>
  <li>In the same repository, have a pipeline (could be any) which allows developers to change/upload pipeline steps
(e.g. edit the steps or run <code class="language-plaintext highlighter-rouge">buildkite-agent pipeline upload</code>).
(Due to the nature of Buildkite and their “dynamic pipelines” feature, this is usually a possibility).</li>
</ol>

<h2 id="the-play">The “Play”</h2>

<ol>
  <li>Make a malicious commit which would fail the security check</li>
  <li>Wait for the security pipeline to fail</li>
  <li>Upload some pipeline YAML containing
```yaml
steps:
    <ul>
      <li>command: echo “gotcha”
 notify:</li>
    </ul>
    <ul>
      <li>github_commit_status:
  context: “buildkite/security-pipeline”
```</li>
    </ul>
  </li>
  <li>Trigger that pipeline on the malicious commit</li>
  <li>Your PR is now mergeable</li>
</ol>

<p>For step 3, it helps if you find a way to upload pipeline YAML outside of the code changes itself
(otherwise you’ll need to get creative in crafting your malicious commit - although most folks
aren’t scrutinizing Buildkite pipeline steps for security flaws).</p>

<h2 id="bonus-every-security-bug-is-a-security-feature">BONUS: Every security bug is a security feature</h2>

<p>Once I discovered this bug, I immediately reported it to Buildkite.</p>

<p>Then, of course, I set out to use this to my advantage.</p>

<p>At the time, I was helping support our usage of GitHub’s Merge Queue.
One displeasure was when parts of CI (say a set of tests) were still running for a particular entry,
where they’d passed on a “downstream” entry. In this scenario we knew it was 99% safe to merge, but
had no way of kicking Buildkite/GitHub into action.</p>

<p>So, easiest way to move forward is… make a pipeline which does the above!
Trigger the pipeline on the SHA of the merge queue entry and ✨ merged ✨</p>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[(for some pre-reading you may want to read my brief post on how GitHub commit statuses and PR required checks work)]]></summary></entry><entry><title type="html">The Problem with GitHub Commit Statuses</title><link href="https://joshcannon.me/2025/08/24/github-commit-status.html" rel="alternate" type="text/html" title="The Problem with GitHub Commit Statuses" /><published>2025-08-24T00:00:00+00:00</published><updated>2025-08-24T00:00:00+00:00</updated><id>https://joshcannon.me/2025/08/24/github-commit-status</id><content type="html" xml:base="https://joshcannon.me/2025/08/24/github-commit-status.html"><![CDATA[<p>GitHub has a feature where you can gate merging Pull Requests behind passing commit statuses.</p>

<p>This is largely used for checks such as “has CI run?”, but can be used for many many more things since this is automatable.</p>

<p>There are all kinds of GitHub Automations leveraging these. Ones for:</p>

<ul>
  <li>Code Coverage gates</li>
  <li>“DO NOT MERGE” checks</li>
  <li>Code Review requirements (because CODEOWNERS isn’t great)</li>
  <li>Pull Request title/description checks</li>
  <li>Merge Queue semantics</li>
  <li>…and of course every AI code reviewer ever</li>
</ul>

<h1 id="how-it-works">How it works</h1>

<p>Just use the API (REST or GraphQL) to send a commit status (passing, failing, or pending) along with a “context” (e.g. a slug)
some optional description (e.g. “you didn’t say the magic word”) and an optional URL.
If configured, the Pull Request MUST have a passing status sent from the exact configured GitHub App 
before it is able to merge.</p>

<h1 id="the-problems">The Problem(s)</h1>

<h2 id="arbitrary-arbitrary-everything-is-arbitrary">Arbitrary, arbitrary. Everything is arbitrary.</h2>

<p>The APIs for commit statuses aren’t limited to apps, and contexts aren’t limited by anything, and URLs aren’t limited, 
and API callers are only limited to those with write access to the repository.</p>

<p>Feeling bored and want to mess with your peers? Pick a PR, grab the SHA and…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh api \
  /repos/{owner}/{repo}/statuses/{SHA} \
   -f 'state=failure' \
   -f 'target_url=https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExcDkwajRocjlpazJ6NXIzbXRlbWg1ZThneml5NzQ4a29sajZkcmZ6MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/owRSsSHHoVYFa/giphy.gif' \
   -f 'description=YOU DIDNT SAY THE MAGIC WORD' \
   -f 'context=i-hate-this-hacker-crap'
</code></pre></div></div>

<p>From a security standpoint, required checks are configured along with the GitHub App they are expected to come from.
Meaning if you’re expecting the required “passes security checks” commit status to come from the GitHub App “Scruff McGruff”,
a passing status from another GitHub App, or from a user, will not be honored.</p>

<p>The status will of course still exist and be displayed, which is some sucky UI/UX. Speaking of…</p>

<h2 id="the-uiux-sucks">The UI/UX sucks</h2>

<p>In addition to aribitrary statuses or descriptions or senders, there’s also the juggling of required status checks with all
the other (not-aribitrary-but-not-required) ones. Not only can it get overwhelming, but it can get downright misleading!</p>

<p>GitHub’s UX changes if you have failing NOT required status checks. The nice green merge button (the one that feels SO GOOD to click)
turns into a clickable-but-greytone one. I can’t count the number of times I’ve had to help people in realizing the button is clickable,
it just isn’t <em>green</em>. I’m also pretty sure as every company matures they make a set of <a href="https://en.wikipedia.org/wiki/Userscript">Userscripts</a> or a Chrome Extension just for GitHub,
and <em>this</em> is the first piece of functionality.</p>

<h2 id="context-is-key">Context is Key</h2>

<p>Lastly, everything is keyed off of the (aribtrary) <code class="language-plaintext highlighter-rouge">context</code> in the payload.</p>

<p>A new status with the same context (but different state or description) just overwrites the previous one.</p>

<p>If you’re adding a new required status - hope all of your opened opened (and currently mergeable) PRs have a passing 
status already.</p>

<p>Or if you want to rename/change the context, good luck!</p>

<h2 id="one-commit-sha-0-to-inifinite-prs">One Commit SHA: 0 to inifinite PRs</h2>

<p>Since commit statuses are sent to commit SHAs that means the relationship between the Pull Request and the “required” statuses
is the same relationship between a branch and a commit.</p>

<p>In the case of Pull Requests, let’s say we have a check that the PR description includes a reference to <a href="https://en.wikipedia.org/wiki/HeadOn">HeadOn</a> (“Apply directly to the forehead”).
And I open a PR against the default branch omitting the reference. I presumably get a failing commit status check and can’t merge my PR.
Then, because our development cycle is weird I also have to open the same change against the <code class="language-plaintext highlighter-rouge">superbowl</code> feature branch, so I do, remembering
the required reference. I get a passing commit status check (apply directly to the commit SHA).</p>

<p>One SHA. One branch (but could be two). Two PRs. Both (now) mergeable.</p>

<p>(The pain of which is felt beyond just the forehead)</p>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[(Just one?)]]></summary></entry><entry><title type="html">Python Namespace Packages are a pain</title><link href="https://joshcannon.me/2025/08/16/py-namespace-packages.html" rel="alternate" type="text/html" title="Python Namespace Packages are a pain" /><published>2025-08-16T00:00:00+00:00</published><updated>2025-08-16T00:00:00+00:00</updated><id>https://joshcannon.me/2025/08/16/py-namespace-packages</id><content type="html" xml:base="https://joshcannon.me/2025/08/16/py-namespace-packages.html"><![CDATA[<p>TL;DR: Just use <code class="language-plaintext highlighter-rouge">__init__.py</code> in every directory. Please.</p>

<h1 id="background">Background</h1>

<p>This blog post is about Python “namespace packages”. Unfortunately because the Zen of Python didn’t include “There should be one– and preferably only one –defintion for a term”, I have to disambiguate.</p>

<p>I assume when you read “Python package” your first thought is of packages as found on the Python Package Index
(PyPI for short, pronunciation: “🥧 P 👁️”). That is mine as well. However, in the statement <code class="language-plaintext highlighter-rouge">import x.y</code>, Python calls <code class="language-plaintext highlighter-rouge">x</code> a 
<a href="https://docs.python.org/3/tutorial/modules.html#modules">“module”</a> as well as a 
<a href="https://docs.python.org/3/tutorial/modules.html#packages">“package”</a>, and <code class="language-plaintext highlighter-rouge">y</code> a “submodule” and a “subpackage”.</p>

<p>And now a tongue twister: “<a href="https://github.com/encukou">Petr</a> promptly packaged Python package <a href="https://github.com/fedora-python/Pello/tree/master/pello"><code class="language-plaintext highlighter-rouge">pello</code></a> producing Python package <code class="language-plaintext highlighter-rouge">pello</code>, then published Python package <code class="language-plaintext highlighter-rouge">pello</code> containing Python package <code class="language-plaintext highlighter-rouge">pello</code> to the <a href="https://pypi.org/project/Pello/">Python Package Index</a> promptly.”</p>

<p>…whew!</p>

<p>In order to disambiguate, we can use the term
<a href="https://packaging.python.org/en/latest/glossary/#term-Distribution-Package">“distribution packages”</a>
to refer to the PyPI-flavored ones 
and <a href="https://packaging.python.org/en/latest/glossary/#term-Import-Package">“import packages”</a><br />
for the module-flavored ones.</p>

<h2 id="definitions-and-terminology">Definition(s) and Terminology</h2>

<p><a href="https://peps.python.org/pep-0420/">PEP 420</a> starts with</p>

<blockquote>
  <p>Namespace packages are a mechanism for splitting a single Python package across multiple directories on disk.</p>
</blockquote>

<p>A better definition might be</p>

<blockquote>
  <p>Namespace packages are a mechanism for splitting a single Import Package across one or more directories on disk.
They can either be “explicit” or “implicit”, depending on the mechanism.</p>
</blockquote>

<p><a href="https://docs.python.org/3/glossary.html#term-namespace-package">The Python docs</a> define namespace packages as:</p>

<blockquote>
  <p>A package which serves only as a container for subpackages. […]</p>
</blockquote>

<p>This is the most correct, in my opinion, because Python import packages don’t have to come from directories on disk.
They may also come from other exotic places like <a href="https://docs.python.org/3/library/zipimport.html">zip archives</a>.</p>

<h2 id="rationale">Rationale</h2>

<p>Let’s say you’re a megacorp named Gooble and you want all of your code to be known to be Gooble code so you’d prefer
all of your modules to start with <code class="language-plaintext highlighter-rouge">gooble.</code>. You now have a choice. Put everything inside of one distribution package named
<code class="language-plaintext highlighter-rouge">gooble</code> (combining code for your Earth Engine with Big Lake code along with your Cloud APIs - 
much to the confusion of Hydrologists everywhere). Or split things into logical sections, each <em>sharing</em> a piece of 
the <code class="language-plaintext highlighter-rouge">gooble.</code> pie, making <code class="language-plaintext highlighter-rouge">gooble</code> a namespace package.</p>

<p>You may be thinking “ok, so what’s the big deal?” - and in most “vanilla” cases there’d be no big deal. Most of the time you
install a Python package, it goes into some <code class="language-plaintext highlighter-rouge">site-packages</code> directory along with all its installed brothers and sisters
and cousins. Install <code class="language-plaintext highlighter-rouge">gooble-storage</code> and <code class="language-plaintext highlighter-rouge">site-packages/gooble/storage/...</code> exists.
Likewise install <code class="language-plaintext highlighter-rouge">gooble-functions</code> and <code class="language-plaintext highlighter-rouge">site-packages/gooble/functions/...</code> will join in.</p>

<p>There are other ways of loading modules from disk, however. And therein lies the challenge.</p>

<h2 id="the-challenge">The challenge</h2>

<p>The reason the code in your <code class="language-plaintext highlighter-rouge">site-packages</code> is found when you <code class="language-plaintext highlighter-rouge">import</code> is due to Python’s default <a href="https://docs.python.org/3/glossary.html#term-path-based-finder">path based finder</a>.
It works kinda like this:</p>

<ul>
  <li>Given a module (like <code class="language-plaintext highlighter-rouge">gooble</code>), iterate through the paths in <code class="language-plaintext highlighter-rouge">sys.path</code> trying to find a valid <code class="language-plaintext highlighter-rouge">gooble</code> module underneath.
This may look like <code class="language-plaintext highlighter-rouge">gooble/__init__.py</code> or <code class="language-plaintext highlighter-rouge">gooble.py</code> or any of the other valid ways of defining a Python module on disk.</li>
  <li>Given a submodule (like <code class="language-plaintext highlighter-rouge">gooble.firewall</code>), first load module <code class="language-plaintext highlighter-rouge">gooble</code>, take the path we found it at (<code class="language-plaintext highlighter-rouge">__path__</code>) and look
underneath for submodule <code class="language-plaintext highlighter-rouge">firewall</code>.</li>
</ul>

<p>Undoubtedly, your <code class="language-plaintext highlighter-rouge">sys.path</code> has your <code class="language-plaintext highlighter-rouge">site-packages</code> directory in it, and this is how most “third-party” code is
loaded most of the time.</p>

<p>But imagine with me, instead of a single <code class="language-plaintext highlighter-rouge">sys.path</code> entry containing every installed distribution package we instead
used one <code class="language-plaintext highlighter-rouge">sys.path</code> entry for <em>each</em> distribution package. This is certainly an attractive idea if you were designing
a Python environment manager with an emphasis on speed. Each package could be installed, in parallel, into a cache directory
and then in order to construct a Python environment you’d simply need to populate <code class="language-plaintext highlighter-rouge">sys.path</code> with the correct cache directories.</p>

<p>Re-enter <code class="language-plaintext highlighter-rouge">gooble</code>. How should the default path finder find <code class="language-plaintext highlighter-rouge">gooble.firestore</code>, given it already loaded <code class="language-plaintext highlighter-rouge">gooble</code> and <code class="language-plaintext highlighter-rouge">gooble.firebase</code> from <code class="language-plaintext highlighter-rouge">/cached/gooble-firebase/gooble/firebase.py</code>?</p>

<p>Namespace packages is the how. They are mechanism for splitting a single Import Package across one or more directories on disk.</p>

<h1 id="namespace-packages">Namespace Packages</h1>

<p>So now we know what namespace packages aim to solve. Its a way to tell the import machinery that an import package belongs
to many (or none, depending on how you want to look at it) distribution packages, and all that entails.</p>

<p>Historically, there were only <em>explicit</em> namespace packages (I suspect at the time they were just called “namespace packages”,
and then PEP 420 came along and gave us <em>implicit</em> namespace packages). Of course, the Zen of Python does say
“There should be one– and preferably only one –obvious way to do it” but I’m not sure it applies here, given it isn’t called
“the Zen of Python Packaging”. I suspect that rule was skipped over and the rule which reads 
“Namespaces are one honking great idea – let’s do more of those!” was instead double-clicked on.</p>

<h2 id="explicit-namespace-packages">Explicit Namespace Packages</h2>

<p>These come in two flavors, but they both accomplish roughly the same thing:
declaring a Python import package as a namespace package. In your namespace package (<code class="language-plaintext highlighter-rouge">gooble/__init__.py</code> in our case), use</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pkgutil</span> <span class="kn">import</span> <span class="n">extend_path</span>
<span class="n">__path__</span> <span class="o">=</span> <span class="n">extend_path</span><span class="p">(</span><span class="n">__path__</span><span class="p">,</span> <span class="n">__name__</span><span class="p">)</span>
<span class="c1"># or...
</span><span class="kn">import</span> <span class="nn">pkg_resources</span>
<span class="n">pkg_resources</span><span class="p">.</span><span class="n">declare_namespace</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
</code></pre></div></div>

<p>These use <code class="language-plaintext highlighter-rouge">__path__</code> shenanigans to trick the Python import machinery into searching additional directories when
searching for submodules.</p>

<p>It is important to note that one quirk of this solution is that <em>every</em> <code class="language-plaintext highlighter-rouge">gooble/__init__.py</code> in <em>every</em> distribution package
MUST agree on doing this. Otherwise, if <code class="language-plaintext highlighter-rouge">gooble-cloud-walk</code> accidentally shipped an empty <code class="language-plaintext highlighter-rouge">gooble/__init__.py</code> <em>AND</em> it had
an “early” <code class="language-plaintext highlighter-rouge">sys.path</code> entry, then it claims the import package <code class="language-plaintext highlighter-rouge">gooble</code> all to itself.</p>

<p>(Remember this way is the “legacy” way)</p>

<h2 id="implicit-namespace-packages">Implicit Namespace Packages</h2>

<p>PEP 420 gave us a (better) alternative, implicit namespace packages. These are easy to make and easy to explain.
Just don’t make <code class="language-plaintext highlighter-rouge">gooble/__init__.py</code>. Easy peasy lemon squeezy. The import machinery was changed to allow this
new way of saying “hey keep looking for <code class="language-plaintext highlighter-rouge">gooble.cloud_build</code> if <code class="language-plaintext highlighter-rouge">gooble/</code> has no <code class="language-plaintext highlighter-rouge">__init__.py</code>.</p>

<p>We still have to make sure every <code class="language-plaintext highlighter-rouge">gooble</code> distribution package agrees <em>NOT</em> to contain the file, but now we don’t have
to worry about multiple distribution packages containing the same path.</p>

<p>(If you’ve ever forgotten to make a <code class="language-plaintext highlighter-rouge">__init__.py</code> file and importing still worked - this is why)</p>

<h1 id="the-pain-of-python-namespace-packages">The Pain of Python (Namespace Packages)</h1>

<p>Unfortunately, both <em>explicit</em> and <em>implicit</em> namespace packages still exist today - meaning we have two ways of accomplishing
the same thing (two strikes against “the Zen”  - one for having “one obvious way” and one for “explicit is better than implicit”).</p>

<p>Secondly, newcomers (or forgetful old-timers) might omit the <code class="language-plaintext highlighter-rouge">__init__.py</code> and now implicitly have an implicit namespace package 
(as opposed to someone adhering to PEP 420 and explicitly having an implicit namespace package).
There’s also a slight import performance hit when doing this (but likely not enough to matter).</p>

<p>Third, for hard-workin’ folks like myself who would like to <a href="https://joshcannon.me/2024/07/05/package-names.html">associate module (and submodule) names to packages</a>,
this is nigh impossible. It’s impossible to correctly determine if the omission of a <code class="language-plaintext highlighter-rouge">__init__.py</code> was implicit or explicit.
Educated guesses can be made, but as we can see from that blog post they are just guesses.</p>

<p>Fourth, implicit namespace packages are still brittle. Developer X knows that <code class="language-plaintext highlighter-rouge">gooble/</code> is a namespace package and omits <code class="language-plaintext highlighter-rouge">__init__.py</code>.
But it can very easily be added. Consider how many Pull Requests you’ve reviewed where you’ve scrutinized the addition of a <code class="language-plaintext highlighter-rouge">__init__.py</code> file (hint: it’s likely an integer less than one). This is because explicit really is better than implicit.</p>

<p>Last, because most installations involve overlapping unpacking in a shared directory (<code class="language-plaintext highlighter-rouge">site-packages</code>), there are several
distribution packages which have namespace import packages, but lack the mechanism. It’s all too easy to have <code class="language-plaintext highlighter-rouge">gooble-deploy</code> 
and <code class="language-plaintext highlighter-rouge">gooble-console</code> contain <code class="language-plaintext highlighter-rouge">gooble/__init__.py</code>, and everything “just work” <em>most of the time</em>. This is a large reason
environment-management tools <em>don’t</em> use the <code class="language-plaintext highlighter-rouge">sys.path</code> trick. It exposes annoying and hard-to-diagnose bugs.</p>

<h1 id="conclusion">Conclusion</h1>

<p>Namespace packages are a useful mechanism which solves a real problem. However, the precise semantics come at the cost of 
cognitive complexity, especially for developer tool authors/maintainers. Here are my suggestions:</p>

<ul>
  <li>Avoid accidental namespace packages - always include <code class="language-plaintext highlighter-rouge">__init__.py</code> files in directories</li>
  <li>Avoid purposeful namespace packages - they’re a pain in the ass</li>
</ul>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://peps.python.org/pep-0420/">PEP 420</a></li>
  <li><a href="https://packaging.python.org/en/latest/guides/packaging-namespace-packages/">Packaging Namespace Packages</a></li>
  <li><a href="https://docs.python.org/3/glossary.html#term-namespace-package">Python Glossary - “Namespace Package”</a></li>
  <li><a href="https://docs.python.org/3/reference/import.html#reference-namespace-package">Python Language Reference - “Namespace Packages”</a></li>
</ul>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[TL;DR: Just use __init__.py in every directory. Please.]]></summary></entry><entry><title type="html">Python: Relative vs Absolute Imports</title><link href="https://joshcannon.me/2025/07/19/relative-imports.html" rel="alternate" type="text/html" title="Python: Relative vs Absolute Imports" /><published>2025-07-19T00:00:00+00:00</published><updated>2025-07-19T00:00:00+00:00</updated><id>https://joshcannon.me/2025/07/19/relative-imports</id><content type="html" xml:base="https://joshcannon.me/2025/07/19/relative-imports.html"><![CDATA[<p>In the PyTexas Discord my fellow PyTexas organizer and friend, Mason Egger, asked:</p>

<blockquote>
  <p>Do you prefer absolute or relative imports for inter-module imports within applications?</p>
</blockquote>

<p>Go ahead and <a href="https://youtu.be/wgpytjlW5wU?si=qoLqiNyuv0EJPpqD">call me a Sith</a> because I deal in absolutes.</p>

<p>And here’s why.</p>

<h1 id="ways-of-running-python-files">Ways of running Python files</h1>

<p>Python has more than a few ways of running a “Python file”:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">python path/to/file.py</code>: Run the file at this path</li>
  <li><code class="language-plaintext highlighter-rouge">python -m path.to.file</code>: Run the module found with this dotted name</li>
  <li><code class="language-plaintext highlighter-rouge">python path/to</code>: Run the <code class="language-plaintext highlighter-rouge">__main__.py</code> file under this directory path</li>
  <li><code class="language-plaintext highlighter-rouge">python -m path/to</code>: Run the module found with this dotted name, which this time would be the directory’s <code class="language-plaintext highlighter-rouge">__init__.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">python path/to/zipapp.zip</code>: Run this <a href="https://docs.python.org/3/library/zipapp.html">zipapp</a></li>
  <li><code class="language-plaintext highlighter-rouge">pytest</code> or any other installed script name: Run the script found at <code class="language-plaintext highlighter-rouge">&lt;Python environment directory&gt;/bin/&lt;name&gt;</code>, e.g. <code class="language-plaintext highlighter-rouge">.venv/bin/pytest</code></li>
</ul>

<h1 id="ways-of-importing-your-fellow-module">Ways of importing your fellow module</h1>

<ul>
  <li><code class="language-plaintext highlighter-rouge">from . import sibling</code>: Imports <code class="language-plaintext highlighter-rouge">sibling</code> from under the same/parent directory this module is in
    <ul>
      <li>This first would try to import attribute <code class="language-plaintext highlighter-rouge">sibling</code> from <code class="language-plaintext highlighter-rouge">__init__.py</code></li>
      <li>then try to import the module named <code class="language-plaintext highlighter-rouge">sibling</code> (either <code class="language-plaintext highlighter-rouge">sibling/__init__.py</code> or <code class="language-plaintext highlighter-rouge">sibling.py</code>)</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">from .sibling import boogers</code>: Same idea, but this time we need to load module <code class="language-plaintext highlighter-rouge">sibling</code> and attribute/submodule <code class="language-plaintext highlighter-rouge">boogers</code></li>
  <li><code class="language-plaintext highlighter-rouge">from .. import uncle</code>: Imports <code class="language-plaintext highlighter-rouge">uncle</code> from under the grandparent directory (using similar rules)</li>
  <li><code class="language-plaintext highlighter-rouge">from ..uncle import tickles</code>: (you get the jist)</li>
  <li><code class="language-plaintext highlighter-rouge">from ... import great_aunt</code>: …</li>
</ul>

<p>These are all examples of <em>relative</em> imports. What they import is <em>relative</em> to the module importing them.</p>

<p>(By the way, in case you needed one, here’s an example of an <em>absolute</em> import: <code class="language-plaintext highlighter-rouge">from pkgname.child import fun</code>)</p>

<h1 id="ways-of-running-python-files-which-import-their-fellow-modules">Ways of running Python files which import their fellow modules</h1>

<p>Given this <a href="https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/">flat layout</a> project:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├── macaroni/
│   ├── __init__.py
│   ├── salad.py
│   └── and_/
│       ├── __init__.py
│       └── cheese.py
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Contents / Invocation</th>
      <th><code class="language-plaintext highlighter-rouge">python -m macaroni.and_.cheese</code></th>
      <th><code class="language-plaintext highlighter-rouge">python macaroni/and_/cheese.py</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">from ..salad import dressing</code></td>
      <td>🎉</td>
      <td>💥 <code class="language-plaintext highlighter-rouge">ImportError: attempted relative import with no known parent package</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">from macaroni.salad import dressing</code></td>
      <td>🎉</td>
      <td>🎉</td>
    </tr>
  </tbody>
</table>

<h1 id="why-is-that">Why is that?</h1>

<p>Buried in the Python docs’ tutorials, in Chapter 6 “Modules”, Section 4 “Packages”, Subsection 2 “Intra-package References” (<a href="https://docs.python.org/3/tutorial/modules.html#intra-package-references">6.4.2. here</a>),
there is this paragraph <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>:</p>

<blockquote>
  <p>Note that relative imports are based on the name of the current module’s package. Since the main module does not have a package, modules intended for use as the main module of a Python application must always use absolute imports.</p>
</blockquote>

<p>When Python encounters a relative import, <a href="https://docs.python.org/3/reference/import.html#package-relative-imports">it is relative to the current package</a>,
which for the “main module” (the module being invoked), doesn’t exist.</p>

<p><strong>The bottom line</strong>: Absolute imports work consistently irrespective of how your Python file is executed. Relative imports don’t give you that luxury.</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>The paragraph used to read differently, but it was also slightly incorrect so I suggested a change in <a href="https://github.com/python/cpython/pull/136846">PR 136846</a>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Josh Cannon</name></author><summary type="html"><![CDATA[In the PyTexas Discord my fellow PyTexas organizer and friend, Mason Egger, asked:]]></summary></entry></feed>