<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet href="https://lukeplant.me.uk/assets/xml/atom.xsl" type="text/xsl media="all"?>
<feed xml:lang="en" xmlns="http://www.w3.org/2005/Atom">
  <title>Luke Plant's home page</title>
  <id>https://lukeplant.me.uk/blog/atom/index.xml</id>
  <updated>2026-03-16T15:49:28Z</updated>
  <author>
    <name>Luke Plant</name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://lukeplant.me.uk/blog/atom/index.xml"/>
  <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/"/>
  <generator uri="https://getnikola.com/">Nikola</generator>
  <entry>
    <title>When perfection is table stakes</title>
    <id>https://lukeplant.me.uk/blog/posts/when-perfection-is-table-stakes/</id>
    <updated>2026-03-16T15:37:13Z</updated>
    <published>2026-03-16T15:37:13Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/when-perfection-is-table-stakes/"/>
    <summary type="html">&lt;p&gt;Some perspectives on approaching a rewrite of a software project.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;For a rewrite or a replacement of existing software, very often you will find that the minimum you must achieve is something that works at least as good as the previous system in every respect, for all your users all of the time. Perfection is &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Table_stakes"&gt;table stakes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is quite annoying if you happen to have the job of doing the rewrite, and not understanding this is a source of agro.&lt;/p&gt;
&lt;p&gt;Here are a couple of examples from the Open Source world:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://www.freedesktop.org/wiki/Software/PulseAudio/"&gt;Pulseaudio&lt;/a&gt; is a
“sound server” for Linux. A sound server is the part of your audio system that
manages the fact that multiple applications might all want to be playing at
once, and the multiple sound input/output devices you might be using.&lt;/p&gt;
&lt;p&gt;Before Pulseaudio there was a different system, and the first time my system
was “upgraded” to Pulseaudio, it had poor sound quality for some apps. I’m not
an audiophile, but the cracks and pops and hissing were very obvious to me.&lt;/p&gt;
&lt;p&gt;Now, compared to the previous system, Pulseaudio also came with a bunch of new
features, like the ability to have a per-application volume control, and do
sound over Bluetooth more easily. But I didn’t care about those features I had
been given (I didn’t need them), while I certainly cared about music sounding
bad. Perfect sound quality, all the time, is the minimum you have to achieve
as a sound server.&lt;/p&gt;
&lt;p&gt;I also found that if I uninstalled Pulseaudio, then I got back to the previous
state where I had ALSA which just worked, and everything sounded fine. So I
was pretty annoyed at this “upgrade”.&lt;/p&gt;
&lt;p&gt;That was some years ago, and these days Pulseaudio has now apparently been
replaced by &lt;a class="reference external" href="https://pipewire.org/"&gt;Pipewire&lt;/a&gt; in my current Linux system.
This happened without me knowing it, and in fact this is one of the highest
compliments you can give to a rewrite or replacement system: &lt;strong&gt;the users
didn’t notice&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More ambitiously in Linux world, there is &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Wayland_(protocol)"&gt;Wayland&lt;/a&gt;, a project started in
2008 which is attempting to replace the &lt;a class="reference external" href="https://en.wikipedia.org/wiki/X_Window_System"&gt;X Window System&lt;/a&gt; as the protocol for how to
do a windowing system, graphics output and keyboard/mouse input (so, almost
everything you consciously interact with on a computer). Unsurprisingly, this
is a lot of work, and &lt;a class="reference external" href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/"&gt;there are still lots of things that don’t work for some
people&lt;/a&gt;, and
&lt;a class="reference external" href="https://www.semicomplete.com/blog/xdotool-and-exploring-wayland-fragmentation/"&gt;lots of problems that people didn’t have before&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Such reports are going to be pretty frustrating for the creators of the new
system, whose only reward for decades of work, usually done for free, is a
long list of complaints.&lt;/p&gt;
&lt;p&gt;But for anyone who is forced to switch, if they find any regression in
anything at any level, they are going to say “this sucks”. And fair enough -
why should they care about all the work you did when the end result for them
is worse? Why should they care about all the new features, which they don’t
yet need (or don’t know they need), when some of the existing features they
definitely do need are now broken?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, what is the lesson? Don’t bother?&lt;/p&gt;
&lt;p&gt;Rather, I think it is this: go ahead and do the rewrite, just buckle up, and set your expectations properly.&lt;/p&gt;
&lt;p&gt;Until it has reached 100% of what it is supposed to replace, you will mostly get complaints. Don’t expect to be thanked for all your work, whether it is paid and commercial or free Open Source development. Nobody cares about the fantastic architectural improvements (which mainly help you, not them), nor about the new features they aren’t using yet. Don’t complain about the complainers and the naysayers, or ask them to shut up, or pretend that their problems don’t exist - why shouldn’t they complain, when you made their life worse?&lt;/p&gt;
&lt;p&gt;(Do the complainers actually have a right to complain? As a side note, when talking about this with my wife, her view was that they really don’t, and maybe they should think about paying for this free software before complaining. This is coming from the perspective of “a wife of an Open Source project maintainer who spends a lot of hours on projects for which he is not thanked (or paid)”. But however much truth there is in that perspective, people &lt;strong&gt;will&lt;/strong&gt; complain, and I don’t think it is sensible to assume otherwise).&lt;/p&gt;
&lt;p&gt;I’m writing this down partly for myself, as I start a project rewriting a
working piece of software for a client. This rewrite will include a large
interface overhaul as well as a lot of internal architecture changes, and I’m
hoping that the interface changes will be welcome.&lt;/p&gt;
&lt;p&gt;However, I have to prepare for the fact that initial reactions will likely be
negative, or not nearly as positive as we might expect, because just by
&lt;strong&gt;changing anything&lt;/strong&gt; I’ve already made someone’s life worse: having to relearn
how to do something they used to know is not most people’s idea of fun. To make
up for that, if I want them to be positively enthusiastic, not only do I have to
cater for every need and fulfil every previous expectation, the improvements
have also got to be both large and obvious — it’s got to be an absolute slam
dunk, from the very first time they try it.&lt;/p&gt;
&lt;p&gt;We’ve got a tiny number of current users for this application, so compared to the other projects I mentioned, the stakes are much lower and we should be able to accommodate everyone’s needs. But getting my expectations right, having empathy with the user, is essential.&lt;/p&gt;</content>
    <category term="software-development" label="Software development"/>
  </entry>
  <entry>
    <title>Announcing fluent-codegen</title>
    <id>https://lukeplant.me.uk/blog/posts/announcing-fluent-codegen/</id>
    <updated>2026-03-10T12:33:49Z</updated>
    <published>2026-03-10T12:33:49Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/announcing-fluent-codegen/"/>
    <summary type="html"></summary>
    <content type="html">&lt;p&gt;This post announces &lt;a class="reference external" href="https://github.com/spookylukey/fluent-codegen"&gt;fluent-codegen&lt;/a&gt;, a Python library for generating Python code. Not everyone needs this, but when you need it, you’ll probably know, and you’ll want probably want a decent solution that is not based on string concatenation.&lt;/p&gt;
&lt;p&gt;The history of this project is that it started out as a &lt;code class="docutils literal"&gt;codegen&lt;/code&gt; module in &lt;a class="reference external" href="https://github.com/django-ftl/fluent-compiler"&gt;fluent-compiler&lt;/a&gt;, which is an implementation of &lt;a class="reference external" href="https://projectfluent.org/"&gt;Project Fluent&lt;/a&gt;, Mozilla’s internationalisation solution. So the word “fluent” referred to that originally, but now it refers to a set of nice APIs for building up Python expressions.&lt;/p&gt;
&lt;p&gt;As I needed a code generation library for another purpose, I pulled out this library and fleshed it out into a relatively complete, standalone project. It has also evolved quite a bit since its earlier form, and is a pretty well rounded library now. You can check out &lt;a class="reference external" href="https://fluent-codegen.readthedocs.io/en/latest/"&gt;the nice docs&lt;/a&gt;, which include a nice little toy example of a “SVG to Python Turtle” compiler. Enjoy!&lt;/p&gt;</content>
    <category term="python" label="Python"/>
  </entry>
  <entry>
    <title>Help my website is too small</title>
    <id>https://lukeplant.me.uk/blog/posts/help-my-website-is-too-small/</id>
    <updated>2025-12-19T13:45:33Z</updated>
    <published>2025-12-19T13:45:33Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/help-my-website-is-too-small/"/>
    <summary type="html">&lt;p&gt;How can it be a real website if it’s less than 7k?&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;A jobs web site I belong to just emailed me, telling me that some of the links in my public profile on their site are “broken” and “thus have been removed”.&lt;/p&gt;
&lt;p&gt;The evidence that these sites are broken? They are too small:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.djangoproject.com/"&gt;https://www.djangoproject.com/&lt;/a&gt;: response body too small (6220 bytes)&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.cciw.co.uk/"&gt;https://www.cciw.co.uk/&lt;/a&gt;: response body too small (3033 bytes)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first is the home page of the Django web framework, and is, unsurprisingly, implemented using Django (see the &lt;a class="reference external" href="https://github.com/django/djangoproject.com"&gt;djangoproject.com source code&lt;/a&gt;). The second is one of my own projects, and also implemented using Django (source &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/"&gt;also available&lt;/a&gt; for anyone who cares).&lt;/p&gt;
&lt;p&gt;Checking in webdev tools on these sites gives very similar numbers to the above for the over-the-wire size of the initial HTML (though I get slightly higher figures), so this wasn’t a blip caused by downtime, as far as I can see.&lt;/p&gt;
&lt;p&gt;Apparently, if your HTML is less than 7k, that obviously can’t be a real website, let alone something as ridiculously small as 3k. Even with compression turned up all the way, it’s clearly impossible to return more than an error message with less than &lt;a class="reference external" href="https://minime.stephan-brumme.com/react/18.0.0/"&gt;at least 4k&lt;/a&gt;, right?&lt;/p&gt;
&lt;p&gt;So please can Django get it sorted and add some bloat to their home page, and to their framework, and can someone also send me tips on bloating my own sites, so that my profile links can be counted as real websites? Thanks!&lt;/p&gt;</content>
    <category term="django" label="Django"/>
    <category term="python" label="Python"/>
    <category term="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>Breaking “provably correct” Leftpad</title>
    <id>https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/</id>
    <updated>2025-10-03T21:00:00+01:00</updated>
    <published>2025-10-03T21:00:00+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/"/>
    <summary type="html">&lt;p&gt;Why? Because it’s fun.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;I don’t know much about about formal methods, so a while back I read Hillel Wayne’s post &lt;a class="reference external" href="https://www.hillelwayne.com/post/lpl/"&gt;Let’s prove Leftpad&lt;/a&gt; with interest. However:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;I know Donald Knuth’s famous quote: “Beware of bugs in the above code; I have only proved it correct, not tried it”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I also know how it turned out that &lt;a class="reference external" href="https://research.google/blog/extra-extra-read-all-about-it-nearly-all-binary-searches-and-mergesorts-are-broken/"&gt;code that had been proved correct harboured a bug not found for decades&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I thought I’d take a peek and do some testing on these &lt;a class="reference external" href="https://github.com/hwayne/lets-prove-leftpad"&gt;Leftpad implementations&lt;/a&gt; that are all “provably correct”.&lt;/p&gt;
&lt;nav class="contents" id="contents" role="doc-toc"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#methodology" id="toc-entry-1"&gt;Methodology&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#implementations" id="toc-entry-2"&gt;Implementations&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#liquid-haskell" id="toc-entry-3"&gt;Liquid Haskell&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#java" id="toc-entry-4"&gt;Java&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#lean4" id="toc-entry-5"&gt;Lean4&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#rust" id="toc-entry-6"&gt;Rust&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#some-competition" id="toc-entry-7"&gt;Some competition!&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#results" id="toc-entry-8"&gt;Results&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#comments" id="toc-entry-9"&gt;Comments&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#explanation" id="toc-entry-10"&gt;Explanation&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#what-is-a-character" id="toc-entry-11"&gt;What is a character?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#how-does-a-programming-language-handle-strings" id="toc-entry-12"&gt;How does a programming language handle strings?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#putting-them-together" id="toc-entry-13"&gt;Putting them together&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#what-went-wrong" id="toc-entry-14"&gt;What went wrong?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#lies-damned-lies-and-natural-language" id="toc-entry-15"&gt;Lies, damned lies and natural language&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#what-is-correct" id="toc-entry-16"&gt;What is correct?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#confessions-and-corrections" id="toc-entry-17"&gt;Confessions and corrections&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#response" id="toc-entry-18"&gt;Response&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="methodology"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-1" role="doc-backlink"&gt;Methodology&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’ll pick a few, simple, perfectly ordinary inputs at random, and work out what
I think the output should be. This is a pretty trivial problem so I’m expecting
that all the implementations will match my output. &lt;em&gt;[narrator: He is is expecting no such thing]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I’m also expecting that, even if for some reason I’ve made a mistake, all the implementations will at least match each other. &lt;em&gt;[narrator: More lies]&lt;/em&gt;  They’ve all been proved correct, right?&lt;/p&gt;
&lt;p&gt;Here are my inputs and expected outputs. I’m going to pad to a length of 10, and use &lt;code class="docutils literal"&gt;-&lt;/code&gt; as padding so it can be seen and counted more easily than spaces.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr class="header"&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Expected padding&lt;/th&gt;
&lt;th&gt;Expected Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr class="odd"&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;𝄞&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;---------𝄞&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class="even"&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Å&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;---------Å&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class="odd"&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;𓀾&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;---------𓀾&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class="even"&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;אֳֽ֑&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;---------אֳֽ֑&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class="odd"&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;résumé&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;----résumé&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class="even"&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;résumé&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;----résumé&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;[“ordinary”, “random” - I think my work here is done…]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I’ve used a monospace font so that the right hand side of the outputs all line up as you’d expect.&lt;/p&gt;
&lt;p&gt;Entry 6 is not a mistake, by the way, it just does “e acute” in a different way to entry 5. Nothing to see here, move along…&lt;/p&gt;
&lt;/section&gt;
&lt;section id="implementations"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-2" role="doc-backlink"&gt;Implementations&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Not all of the &lt;a class="reference external" href="https://github.com/hwayne/lets-prove-leftpad/tree/master"&gt;implementations&lt;/a&gt; were that easy to run. In fact, many of them didn’t take any kind of “string” as input, but vectors or lists or such things, and it wasn’t obvious to me how to pass strings in. So I discounted them.&lt;/p&gt;
&lt;p&gt;For the ones I could run, I attempted to do so by embedding the test inputs directly in the program, if possible.&lt;/p&gt;
&lt;section id="liquid-haskell"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-3" role="doc-backlink"&gt;Liquid Haskell&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Embedding the characters directly in Haskell source code kept getting me “lexical error in string/character literal”, so I wrote a small driver program that read from a file.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="java"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-4" role="doc-backlink"&gt;Java&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="reference external" href="https://github.com/hwayne/lets-prove-leftpad/blob/ea9c0f09a2d3e981d82118497c307844fc7b1f49/java/LeftPad.java#L7"&gt;leftpad function provided&lt;/a&gt; didn’t take a string, but a &lt;code class="docutils literal"&gt;char[]&lt;/code&gt;. Thankfully, it’s easy to convert from &lt;code class="docutils literal"&gt;String&lt;/code&gt; objects, using the &lt;code class="docutils literal"&gt;.toCharArray()&lt;/code&gt; function. So I did that.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="lean4"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-5" role="doc-backlink"&gt;Lean4&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There is a handy &lt;a class="reference external" href="https://live.lean-lang.org/"&gt;online playground&lt;/a&gt;, and the implementation had a helpful &lt;code class="docutils literal"&gt;#eval&lt;/code&gt; block that I could modify to get output. You can &lt;a class="reference external" href="https://live.lean-lang.org/#codez=PQWgBAsg9gTgpmAzgSwLYAcA2zEBdkDGYAhgHYAmSuMxyA5gBa4BmsA7sTJQG5wwpRSYKMzC4GcAFBgZmOM1zpilROjgFkzQsXyCSFMOhhQRiMMiG42UML37JBiAFzSZrGCRgAjZNU4BPMGw8MwswOjhSPmJMfUp3KhgLOjNVdU1tTEx/aRBgSUluTmRiLzkwAG9ARuAwJzAAFX81AF8C8nkg+UVlMAAKITqAOR0ASj7iWrAqsd7YuoAZHFwpsYWlqdqAXldFvAA6eCxtXAR+sHBMPblSOnExiYBqB6CC8ThYOFROhSVyAH1rrcGH0BmBhrgZhM6tM+nMwLtljCXDIZLMur8wEIJpgRldIkCwJswKhiAAPTGwvE3O5bMBeHIyFAYMAAbTkP2UABowTo9ogAK5eP7Kf5wACOfxJpIAugVJG8Pl92d1/kZ5MhySyAEIAUTFU2lrPmxDYzH5mF1+qqhrOQ1G40mMNmkwRK1qrlRCIOcCOBB0pyEFypQPuuJwAAV4FpSQB5URojmULFBVZE+muJnoVnK36y14SRXfFV/AXMaOsy0Go0ms0WvVV208iEO6EzOGupEeoJ7HAAZX5ZY1cdh6J6yZxtPTjLQWbZo/IeckpGIqDgqmIBAQveoyUQbQ6OZ6jfBkMmAGEGJwZmY6tukjdVmA78ktq4AERvvbofmIBjJs7gIgwY0k8SAFAAxHARSxIelAAIwAAxgAA5CAyFgG+gAsG4AeLtvvm7zwEq84Avi4ggnaza9MQdQXlefTOE+O4Pl2dQJiqFITIguKAmRRJShSvRATxDCpq49JIDO2bzty4J8oKwrkKKEpSnmCqEYx950Hs5A6MQfzfr+QiCbeTF0DMBA0ZeMAzKQFEjCx9Ffj+f5gEQpC4jpuATESQGeY8zy9F6hzYH6JwUgQqZ0gyYD8qQrCYJQz43E5hmuFGFgIKQ3oEDGQgAPwAF5gHle4yH6iAIEVmwAHwScyLKyYccA6LKZXEBVSCEtVXYWNQNhGcgDCsY1PrNcsvQAO0pcCEUUlx2m6YSSALV5YCgUFPohf64X2SidWziNag6Nyg2tSicCkhuyx6ny/ioF8G2+v6Jb8gQBDIfhhawfp6Xkv0dnjJZdHGRpyS7SirGPVtYUActwmhmAAA8DysbBHFIB5i1OGm0WxfFlCwRmkksklWl+cK6BqBQ3KkytekGX+qkFup32luW/1NpCQPWfRJmaeDMh1L5i2I04AVo8m81+ZOuNxVACVFr8RP1bT5PEJTkTkDTpl0/pzmkNKQA"&gt;play with it here&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="rust"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-6" role="doc-backlink"&gt;Rust&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/hwayne/lets-prove-leftpad/blob/ea9c0f09a2d3e981d82118497c307844fc7b1f49/creusot%20(rust)/src/lib.rs"&gt;The code here&lt;/a&gt; had loads of extra lines regarding specs etc. which I stripped so I could easily run it, which worked fine.&lt;/p&gt;
&lt;p&gt;More tricky was that the code didn’t take a string, but some &lt;code class="docutils literal"&gt;Vec&amp;lt;Thingy&amp;gt;&lt;/code&gt;. As I know nothing about Rust, I got ChatGPT to tell me how to convert from a string to that. It gave me two options, I picked the one that looked simpler and less &amp;lt;&amp;lt;angry&amp;gt;&amp;gt;. I didn’t deliberately pick the one which made Rust look even worse than all the others, out of peevish resentment for every time someone has rewritten some Python code (my go-to language) in Rust and made it a million times faster – that’s a ridiculous suggestion.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="some-competition"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-7" role="doc-backlink"&gt;Some competition!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To make things interesting, let’s compare these provably correct implementations with one vibe-coded by ChatGPT, in some random language, like, say, um, Swift. It gave me this code:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code swift"&gt;&lt;a id="rest_code_01436f3585ee41f18b92225a7131a680-1" name="rest_code_01436f3585ee41f18b92225a7131a680-1" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#rest_code_01436f3585ee41f18b92225a7131a680-1"&gt;&lt;/a&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Foundation&lt;/span&gt;
&lt;a id="rest_code_01436f3585ee41f18b92225a7131a680-2" name="rest_code_01436f3585ee41f18b92225a7131a680-2" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#rest_code_01436f3585ee41f18b92225a7131a680-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_01436f3585ee41f18b92225a7131a680-3" name="rest_code_01436f3585ee41f18b92225a7131a680-3" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#rest_code_01436f3585ee41f18b92225a7131a680-3"&gt;&lt;/a&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;leftPad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Character&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_01436f3585ee41f18b92225a7131a680-4" name="rest_code_01436f3585ee41f18b92225a7131a680-4" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#rest_code_01436f3585ee41f18b92225a7131a680-4"&gt;&lt;/a&gt;    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;paddingCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_01436f3585ee41f18b92225a7131a680-5" name="rest_code_01436f3585ee41f18b92225a7131a680-5" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#rest_code_01436f3585ee41f18b92225a7131a680-5"&gt;&lt;/a&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;paddingCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;a id="rest_code_01436f3585ee41f18b92225a7131a680-6" name="rest_code_01436f3585ee41f18b92225a7131a680-6" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#rest_code_01436f3585ee41f18b92225a7131a680-6"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can &lt;a class="reference external" href="https://swiftfiddle.com/6x45qsd74jasbj4buruikqivne"&gt;play with it online here&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="results"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-8" role="doc-backlink"&gt;Results&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here are the results, green for correct and red for … less correct.&lt;/p&gt;
&lt;style&gt;
  table.results {
      font-family: monospace;
  }
  td.result-pass {
      background-color: light-dark(#00ff00, #004000);
      color: light-dark(black, white);
  }
  td.result-fail {
      background-color: light-dark(#ff8080, #400000);
      color: light-dark(black, white);
  }
&lt;/style&gt;&lt;table class="results"&gt;
  &lt;tr&gt;
    &lt;th&gt;Input&lt;/th&gt;
    &lt;th&gt;Reference&lt;/th&gt;
    &lt;th&gt;Java&lt;/th&gt;
    &lt;th&gt;Haskell&lt;/th&gt;
    &lt;th&gt;Lean&lt;/th&gt;
    &lt;th&gt;Rust&lt;/th&gt;
    &lt;th&gt;Swift&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;𝄞&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------𝄞&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
    &lt;td class="result-fail"&gt;------𝄞&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Å&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------Å&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------Å&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------Å&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------Å&lt;/td&gt;
    &lt;td class="result-fail"&gt;-------Å&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------Å&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;𓀾&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------𓀾&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
    &lt;td class="result-fail"&gt;------𓀾&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;אֳֽ֑&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-fail"&gt;------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-fail"&gt;------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-fail"&gt;------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-fail"&gt;--אֳֽ֑&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------אֳֽ֑&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;&lt;p&gt;And pivoted the other way around so you can compare individual inputs more easily:&lt;/p&gt;
&lt;table class="results"&gt;
  &lt;tr&gt;
    &lt;th&gt;Language&lt;/th&gt;
    &lt;th&gt;𝄞&lt;/th&gt;
    &lt;th&gt;Å&lt;/th&gt;
    &lt;th&gt;𓀾&lt;/th&gt;
    &lt;th&gt;אֳֽ֑&lt;/th&gt;
    &lt;th&gt;résumé&lt;/th&gt;
    &lt;th&gt;résumé&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Reference&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------Å&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Java&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------𝄞&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------Å&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------𓀾&lt;/td&gt;
    &lt;td class="result-fail"&gt;------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Haskell&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------Å&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
    &lt;td class="result-fail"&gt;------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Lean&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
    &lt;td class="result-fail"&gt;--------Å&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
    &lt;td class="result-fail"&gt;------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Rust&lt;/td&gt;
    &lt;td class="result-fail"&gt;------𝄞&lt;/td&gt;
    &lt;td class="result-fail"&gt;-------Å&lt;/td&gt;
    &lt;td class="result-fail"&gt;------𓀾&lt;/td&gt;
    &lt;td class="result-fail"&gt;--אֳֽ֑&lt;/td&gt;
    &lt;td class="result-fail"&gt;--résumé&lt;/td&gt;
    &lt;td class="result-fail"&gt;résumé&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Swift&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𝄞&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------Å&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------𓀾&lt;/td&gt;
    &lt;td class="result-pass"&gt;---------אֳֽ֑&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
    &lt;td class="result-pass"&gt;----résumé&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;&lt;/section&gt;
&lt;section id="comments"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-9" role="doc-backlink"&gt;Comments&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Rust, as expected, gets &lt;a class="reference external" href="https://en.wiktionary.org/wiki/nul_points"&gt;nul points&lt;/a&gt;. What can I say?&lt;/p&gt;
&lt;p&gt;Vibe-coding with Swift: 💯&lt;/p&gt;
&lt;p&gt;Other that, we can see:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;The only item that all implementations (apart from Rust) get correct is entry 5, the first of the two &lt;code class="docutils literal"&gt;résumé&lt;/code&gt; options.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Java is mostly consistent with the others, but it appears it doesn’t like musical notation, or Egyptian hieroglyphics (item 3 is “standing mummy”), which seems a little rude.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The score so far:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fancy-pants languages and formal verification: 0&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vibe-coding it with ChatGPT: 1&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="explanation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-10" role="doc-backlink"&gt;Explanation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;OK, I’ve had my fun now :-)&lt;/p&gt;
&lt;p&gt;(The original “Let’s Prove Leftpad” project was done “because it is funny”, and this post is in the same spirit. I want to be especially clear that &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/"&gt;I’m not actually a fan of vibe-coding&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;What’s actually going on here? There are two main issues, both tied up with the concept of “the length of a string”.&lt;/p&gt;
&lt;p&gt;(If you already know enough about Unicode, or don’t care about the details, you can skip to the “What went wrong?” section to continue discussion regarding formal verification).&lt;/p&gt;
&lt;p&gt;First:&lt;/p&gt;
&lt;section id="what-is-a-character"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-11" role="doc-backlink"&gt;What is a character?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Strings are composed of “characters”, but what are they?&lt;/p&gt;
&lt;p&gt;Most modern computer languages, and all the ones I included above, use Unicode as the basis for answering this. Unicode, at its heart, is a list of “code points” (although it is bit more than this). A code point, however, is not exactly a character.&lt;/p&gt;
&lt;p&gt;Many of the code points you use most often, like &lt;a class="reference external" href="https://unicodeplus.com/U+0041"&gt;Latin Capital Letter A U+0041&lt;/a&gt;, are exactly what you think of as a character. But many are not. These include:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Combining_character"&gt;combining characters&lt;/a&gt;, which are used to add accents and marks to other characters. These have some complexity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First, whether you regard the accent as part of the character or not can be a matter of debate. For example, in &lt;code class="docutils literal"&gt;é&lt;/code&gt;, “e acute”, you might think of this as a different letter to &lt;code class="docutils literal"&gt;e&lt;/code&gt;, or an &lt;code class="docutils literal"&gt;e&lt;/code&gt; plus an acute accent. In some languages, this distinction is critical e.g. in Turkish &lt;code class="docutils literal"&gt;ç&lt;/code&gt; is not just “c with some decoration”, it’s an entirely distinct letter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, Unicode often has multiple ways of generating these accented characters, either “pre-composed” as a single code point, or as a combination of multiple code points. So, there is both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;é: &lt;code class="docutils literal"&gt;Latin Small Letter E With Acute U+00E9&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;é: &lt;code class="docutils literal"&gt;Latin Small Letter E U+0065&lt;/code&gt; + &lt;code class="docutils literal"&gt;Combining Acute Accent U+0301&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://unicode-explorer.com/articles/space-characters"&gt;various spacing characters&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Unicode_control_characters"&gt;control characters&lt;/a&gt; such as bidi isolation characters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and probably more&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, Unicode has another concept, called the “extended grapheme cluster”, or “user-perceived character”, which more closely maps to what you might think of as a “character”. That’s the concept I’m implicitly using for my claim of what leftpad should output.&lt;/p&gt;
&lt;p&gt;Secondly, there is the question of:&lt;/p&gt;
&lt;/section&gt;
&lt;section id="how-does-a-programming-language-handle-strings"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-12" role="doc-backlink"&gt;How does a programming language handle strings?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Different languages have different fundamental answers to the question of “what is a string?”, different internal representations of them (how the data is actually stored), and different ways of exposing strings to the programmer.&lt;/p&gt;
&lt;p&gt;Some languages, especially performance oriented ones, provide little to zero insulation from the internal representation, while others provide a fairly strong abstraction. Some languages, like Haskell, provide &lt;a class="reference external" href="https://hasufell.github.io/posts/2024-05-07-ultimate-string-guide.html#string-types"&gt;multiple string types&lt;/a&gt; (which can be used with string literals in your code with the &lt;a class="reference external" href="https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/overloaded_strings.html"&gt;OverloadedStrings extension&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;At this point, as well as “code points”, we’ve got to consider “encodings”. If you have a code point, even a “simple” one like &lt;code class="docutils literal"&gt;U+0041&lt;/code&gt; (&lt;code class="docutils literal"&gt;A&lt;/code&gt;), you have said nothing about how you are going to store or transmit that data. An encoding is a system for doing that. Two relevant ones here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://en.wikipedia.org/wiki/UTF-8"&gt;UTF-8&lt;/a&gt; is probably the most common/popular one. It uses anything from 1 to 4 bytes to express code points. It has lots of useful properties, but an important one is backwards compatibility with ASCII - if you happen to be limited to the 127 characters found in ASCII, then UTF-8 encoded Unicode text is one-byte-per-codepoint, and is byte-for-byte the the same as ASCII.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://en.wikipedia.org/wiki/UTF-16"&gt;UTF-16&lt;/a&gt; is an encoding where most code points (specifically those in the &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane"&gt;Basic Multilingual Plane or BMP&lt;/a&gt;) take up 2 bytes, and the remainder can be specified using 4 bytes and a system called “surrogate pairs”.&lt;/p&gt;
&lt;p&gt;UTF-16 exists because originally Unicode had less than 65,536 code points, meaning you could represent all points with just two bytes, and it was thought we would never need more.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In terms of languages today, with some simplification we can say the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In Haskell, Lean, Python, and many other languages, &lt;strong&gt;strings are sequences of Unicode code points&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;There is little attempt to hide the idea of code points from you, although in some, like Python, the internal representation itself might be hidden (see &lt;a class="reference external" href="https://peps.python.org/pep-0393/"&gt;PEP 393&lt;/a&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In Java and Javascript, &lt;strong&gt;strings are sequences of UTF-16 items&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Originally strings were intended to be sequences of code points, but when Unicode grew, to avoid breaking all existing Java code, it morphed into the UTF-16 compromise.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In Rust, &lt;strong&gt;strings are UTF-8 encoded Unicode code points&lt;/strong&gt; (see &lt;a class="reference external" href="https://doc.rust-lang.org/std/string/struct.String.html"&gt;String&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Rust does also have an easily accessible &lt;a class="reference external" href="https://doc.rust-lang.org/std/primitive.str.html#method.chars"&gt;chars&lt;/a&gt; method/concept, which corresponds to a Unicode code point. I didn’t use this above - Rust would have behaved the same as Haskell/Lean if I had.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In Swift, &lt;strong&gt;strings are sequences of user-perceived characters&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I’m not a user of Swift, but &lt;a class="reference external" href="https://developer.apple.com/documentation/swift/string"&gt;from the docs&lt;/a&gt; it appears to do a pretty good job of abstracting away from the “sequence of code point” paradigm. For example, iterating over a string gets you the “characters” (i.e. extended grapheme clusters) one by one, and the &lt;code class="docutils literal"&gt;.count&lt;/code&gt; property gives you the number of such characters. It does also have a &lt;code class="docutils literal"&gt;.length&lt;/code&gt; property that gives you the number of code points.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="putting-them-together"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-13" role="doc-backlink"&gt;Putting them together&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;These differences, between them, explain the differences in output above. In more detail:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;the treble clef &lt;code class="docutils literal"&gt;𝄞&lt;/code&gt; is a single code point, but it is outside the BMP and requires 2 UTF-16 items. So Java considers the length to be 2, and only 8 padding characters were added, in contrast to other languages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I created the &lt;code class="docutils literal"&gt;Å&lt;/code&gt; using two code points (although there is a pre-composed version of this character).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;אֳֽ֑&lt;/span&gt;&lt;/code&gt; is Hebrew, and is composed an Aleph followed by multiple vowel marks and a cantillation mark, bringing it up to 4 code points.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;résumé&lt;/code&gt; was spelled in two different ways, one with precomposed &lt;code class="docutils literal"&gt;é&lt;/code&gt; which is one code point, the other with combining characters where each &lt;code class="docutils literal"&gt;é&lt;/code&gt; requires two code points.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;None of the inputs was wholly in the ASCII range, so encoding them as UTF-8 requires more bytes, which is why Rust (as I used it) behaved as it did.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="what-went-wrong"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-14" role="doc-backlink"&gt;What went wrong?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For me, the biggest issue is not the “code points” vs “characters” debate, which is responsible for most of the variation shown, but the issue that resulted in the difference in the Java output i.e. UTF-16. All of the others (if I hadn’t stitched up Rust) would have resulted in the same output at least.&lt;/p&gt;
&lt;p&gt;Apparently, nothing in the process of doing the formal verification forced the implementations to converge, and I think it is pretty fair to conclude that that at least one of the implementations must be faulty, given that they produce different output.&lt;/p&gt;
&lt;p&gt;So what went wrong?&lt;/p&gt;
&lt;/section&gt;
&lt;section id="lies-damned-lies-and-natural-language"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-15" role="doc-backlink"&gt;Lies, damned lies and natural language&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;English (or any natural language) is at the heart of the problem here. We should start with the phrase “provably correct”. It has a technical definition, but I’m not convinced those English words help us. The post accompanying the &lt;a class="reference external" href="https://ucsd-progsys.github.io/liquidhaskell-blog/2018/05/17/hillel-verifier-rodeo-I-leftpad.lhs/"&gt;LiquidHaskell entry for Leftpad&lt;/a&gt; puts it this way:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My eyes roll whenever I read the phrase “proved X (a function, a program) correct”.&lt;/p&gt;
&lt;p&gt;There is no such thing as “correct”.&lt;/p&gt;
&lt;p&gt;There are only “specifications” or “properties”, and proofs that ensures that your code matches those specifications or properties.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For these reasons, I think I’d prefer talking about “formally verified” functions – it at least prompts you to ask “what does that mean”, and maybe suggests that you should be thinking about what, specifically, has been verified.&lt;/p&gt;
&lt;p&gt;The next bit of English to trip us up is “the length of a string”. It’s extremely easy to imagine this is an easy concept, but in the presence of Unicode it really isn’t.&lt;/p&gt;
&lt;p&gt;Hillel’s original informal requirements don’t actually use that phrase, instead they use the pseudo-code &lt;code class="docutils literal"&gt;max(n, len(str))&lt;/code&gt;. Looking at the implementations, it appears people have subconsciously interpreted this as “the length of the string”, and then assumed that the functions like &lt;code class="docutils literal"&gt;length&lt;/code&gt; or &lt;code class="docutils literal"&gt;size&lt;/code&gt; that their language provides do “the right thing”.&lt;/p&gt;
&lt;p&gt;We could conclude this is in fact a problem with informal requirements – it was at the level of interpreting those requirements that this went wrong. Therefore, we need &lt;strong&gt;more&lt;/strong&gt; formal specifications and verification, &lt;strong&gt;not less&lt;/strong&gt;. But I don’t think this gets round the fact that we’ve got to translate at some point, and at that point you’ve got trouble.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="what-is-correct"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-16" role="doc-backlink"&gt;What is correct?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The issue I haven’t addressed so far is whether my reference output and the Swift implementation are actually “correct”. The reality is that you can make arguments for different things.&lt;/p&gt;
&lt;p&gt;Implicitly, I’m arguing that &lt;strong&gt;left pad should be used for visual alignment in a fixed width context&lt;/strong&gt;, and the implementation that does the best at that is the best one. I think that is a pretty reasonable case. But I’m sure you could make a case for other output – there isn’t actually anything that says &lt;strong&gt;what&lt;/strong&gt; left pad should be used for. It’s possible that there are use cases where “the language’s underlying concept of the length of a string, whatever that may be” is the most important thing.&lt;/p&gt;
&lt;p&gt;In addition, I was hiding the fact that “fixed width” is yet another lie:&lt;/p&gt;
&lt;p&gt;I was originally going to use a flag character like 🏴󠁧󠁢󠁥󠁮󠁧󠁿 as one of my inputs, which is a single “extended grapheme cluster” that uses no less then &lt;strong&gt;7 code points&lt;/strong&gt;. It also results in 14 UTF-16 units in Java. The problem was that this character, like most emojis and many other characters from wide scripts like Chinese, takes up a &lt;strong&gt;double width&lt;/strong&gt; even with a monospace font.&lt;/p&gt;
&lt;p&gt;To maintain the subterfuge of “look how these all line up neatly in the correct output”, I was forced to use other examples. In other words, the example use case I was relying on to prove that these leftpad implementations were broken, is itself a broken concept. But I would still maintain that my reference output is closer to what you would expect leftpad to do.&lt;/p&gt;
&lt;p&gt;A big point is this: even if we argue that a give implementation is “correct” (in that it does what its specifications say it does), that doesn’t mean you are &lt;strong&gt;using&lt;/strong&gt; it correctly. Are you using it for its intended purpose and context? That seems like a really hard question to answer even for leftpad, and many other real world functions are similar.&lt;/p&gt;
&lt;p&gt;So, I’m not sure what my final conclusion is, other than “programming is hard, &lt;span class="strike"&gt;let’s go shopping&lt;/span&gt;  let’s eat chocolate” (alternative suggested by my wife, that’s my plan for the evening then).&lt;/p&gt;
&lt;/section&gt;
&lt;section id="confessions-and-corrections"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-17" role="doc-backlink"&gt;Confessions and corrections&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Swift implementation was indeed written by ChatGPT, and it got it right first time, with just the prompt “Implement leftpad in Swift”. However:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Swift is the only language I know where an implementation that does what I wanted it to do is that simple.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I followed up by getting ChatGPT to produce a Python version, and it had all the same problems as Haskell/Lean and similar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I noticed that Swift doesn’t calculate strings lengths the way I would need for my use case for some characters, such as &lt;a class="reference external" href="https://unicodeplus.com/U+200B"&gt;Zero Width Space U+200B&lt;/a&gt; and  &lt;a class="reference external" href="https://unicodeplus.com/U+2067"&gt;Right-To-Left Isolate U+2067&lt;/a&gt;, which I would need to count for zero length.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As mentioned, the other way to use the Rust version has the same behaviour as the Haskell/Lean/etc versions. ChatGPT did actually point out to me that this way was better, and the other way (the one I used) was adequate only if the input was limited to ASCII.&lt;/p&gt;
&lt;p&gt;I do feel &lt;strong&gt;slightly&lt;/strong&gt; justified in my treatment of this solution however - &lt;a class="reference external" href="https://github.com/hwayne/lets-prove-leftpad/blob/ea9c0f09a2d3e981d82118497c307844fc7b1f49/creusot%20(rust)/src/lib.rs#L8"&gt;the provided code&lt;/a&gt; didn’t give a function that actually took a string, nor did it force you to use it in a specific way. It dodged the hard part, so I punished it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="response"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/#toc-entry-18" role="doc-backlink"&gt;Response&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Hillel was kind enough to look at this post, and had this response to add:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In general, formally verified code can “go wrong” in two ways: the proven properties that don’t match what we need, or the proof depends on assumptions that are not true in practice. This is a good example of the former. An example of the latter would be the assumption that the output string is storable in memory. None of the formally verified functions will correctly render &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;leftpad(“-“,&lt;/span&gt; 1e300, “foo”)&lt;/code&gt;. This is why we always need to be careful when talking about “proving correctness”. In formal methods, “correct” &lt;strong&gt;always&lt;/strong&gt; means “conforms to a certain specification under certain assumptions”, which is very different from the colloquial use of “correct” (does what you want and doesn’t have bugs).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He also pointed out that padding/alignment functionality available in standard libraries, like Python’s &lt;a class="reference external" href="https://docs.python.org/3/library/string.html#formatspec"&gt;Format Specification Mini-Language&lt;/a&gt; and Javascript’s &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart"&gt;padStart&lt;/a&gt;, have similar issues.&lt;/p&gt;
&lt;/section&gt;</content>
    <category term="haskell" label="Haskell"/>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
  </entry>
  <entry>
    <title>Devotions on 1 Thessalonians 1, August 2025</title>
    <id>https://lukeplant.me.uk/blog/posts/devotions-on-1-thessalonians-1-august-2025/</id>
    <updated>2025-09-13T15:03:01+01:00</updated>
    <published>2025-09-13T15:03:01+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/devotions-on-1-thessalonians-1-august-2025/"/>
    <summary type="html">&lt;p&gt;Devotions that I gave on “CCiW Purple camp” this summer&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;A few weeks ago on &lt;a class="reference external" href="https://www.cciw.co.uk/camps/2025/purple/"&gt;CCiW Purple Camp&lt;/a&gt;, I led devotions one morning, speaking from 1 Thessalonians 1. A few days ago I was asked if I had a written form, and since I did prepare a full script, here it is.&lt;/p&gt;
&lt;p&gt;I must mention, however, that 100% of the inspiration and insight from these devotions came from an excellent paper by Wayne Grudem, &lt;a class="reference external" href="https://lukeplant.me.uk/blogmedia/Pleasing-God-by-Our-Obedience.pdf"&gt;Pleasing God by Our Obedience&lt;/a&gt;. This paper has been hugely helpful and influential for me over about the past 10 years, and I highly recommend it. Also, &lt;a class="reference external" href="https://www.icmbooks.co.uk/product/17989/The-Hole-in-Our-Holiness"&gt;The Hole in Our Holiness&lt;/a&gt; by Kevin DeYoung has several chapters that enlarge very helpfully on these subjects, and it is again very highly recommended.&lt;/p&gt;
&lt;hr class="docutils"&gt;
&lt;p&gt;We’re going to look briefly at 1 Thessalonians chapter 1, especially v2-3, and some verses from chapter 4.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2 We always thank God for all of you and continually mention you in our prayers. 3 We remember before our God and Father your work produced by faith, your labour prompted by love, and your endurance inspired by hope in our Lord Jesus Christ.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now I cannot claim that I am as faithful in my praying for all of you as the apostle Paul was in his prayers for the Thessalonians. However, when I think of you – some of whom I know better than others – when I think of the people here I do feel the same way as Paul did. I think of people who love God and have been faithfully following him and serving him, and it brings me great joy.&lt;/p&gt;
&lt;p&gt;The apostle Paul had confidence that these people were real believers because of the way that they accepted the message, and then changed in their behaviour - v4-10. The Holy Spirit had so obviously been active among them, and continued to be active – especially in producing faith, love and hope, which were very visible in the lives of the Thessalonians.&lt;/p&gt;
&lt;p&gt;When I look around at the people here, I see the same things:&lt;/p&gt;
&lt;p&gt;I see work produced by faith - God has promised to build his kingdom through the proclamation of his word, and you believe that, and that’s a huge part of why you bother with all this. I see labour prompted by love – I see people who make sacrifices in order to come on camp, giving up time and holiday and money, and I know that many of you do much more throughout the year. You do it because you love Jesus and his people and love the world who need to hear the message. And I see endurance inspired by hope in our Lord Jesus – many of you have been doing this for many years.&lt;/p&gt;
&lt;p&gt;When we see the same graces that Paul saw in the Thessalonians, we should come to the same conclusion. As evangelicals, sometimes we are very quick to talk about all the ways we fail, but much less quick to recognise that God is at work in us, and that he is succeeding.&lt;/p&gt;
&lt;p&gt;1 Thessalonians 4:1, a little later, says this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As for other matters, brothers and sisters, we instructed you how to live in order to please God, as in fact you are living. Now we ask you and urge you in the Lord Jesus to do this more and more.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Paul is saying you are already living in a way that pleases God.&lt;/p&gt;
&lt;p&gt;There are multiple ways in which God is pleased with his people. When you wake up in the morning, before you’ve done anything good or bad, you can say “God is pleased with me today, because I’m in Christ, and all his righteousness is counted to me.” That’s justification, it’s absolutely wonderful and foundational.&lt;/p&gt;
&lt;p&gt;But that’s not what I’m talking about here. God is also pleased with us because of our behaviour. And sometimes we struggle with this a bit more. Sometimes we even say “nothing we do pleases God, all our works are like filthy rags”. That may be true for the unbeliever, but it’s not for the believer. Our good works are described in the Bible as &lt;strong&gt;good&lt;/strong&gt; works, not bad works. That doesn’t mean they are perfect. There may be some sin in them, there may be mixed motives, or failure of various kinds. But they are still good works, and they bring God pleasure.&lt;/p&gt;
&lt;p&gt;The Holy Spirit is active in us just as he was in the Thessalonians to produce things that are genuinely good, that bring pleasure to our Father in heaven. And he’s a heavenly Father who is so much better than any human father. That means for one that he’s never a nit-picker. He’s not a fault finder. &lt;strong&gt;He’s not hard to please.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One of the really importance consequences of this will be joy and motivation.&lt;/p&gt;
&lt;p&gt;Imagine, for a second, the gender-stereotypical married couple, where the wife stays at home and puts a lot of effort into looking after the house and cooking meals, and the husband goes out to work. Imagine that the husband, after finishing the meal the wife has prepared, every single time, says “I want you to know that, despite the meal you have made me, I will always love you”. How’s she going to feel? Isn’t she going to be very depressed? If she thinks that with all her efforts no meal she makes ever brings him any pleasure, that he doesn’t enjoy it at all – isn’t she going to be miserable? Where will she get motivation from?&lt;/p&gt;
&lt;p&gt;In the same way, we will live miserable Christian lives if we think our service of God is never good enough to please Him. No! We need to understand that our labour prompted by love does bring God pleasure, by God’s power we are succeeding in our aim! (Even if not perfectly). It is the normal Christian life to lie down at the end of a day and think “I’m living the way my heavenly Father wants, and he is pleased with my service today”. And then, you’ll be motivated to do the “more and more” bit.&lt;/p&gt;
&lt;p&gt;The message is this - God is pleased with all your love and faith at work this week, it has brought a smile to his face. So &lt;strong&gt;enjoy that smile&lt;/strong&gt;, and let the joy of knowing that smile motivate you to keep going, and do more and more.&lt;/p&gt;</content>
    <category term="christianity" label="Christianity"/>
  </entry>
  <entry>
    <title>Why I’m not letting the juniors use GenAI for coding</title>
    <id>https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/</id>
    <updated>2025-07-26T21:30:00+01:00</updated>
    <published>2025-07-26T21:30:00+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/"/>
    <summary type="html">&lt;p&gt;TLDR: because I want them to become seniors one day, and I want them to enjoy being developers&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In my current project, I am training some junior developers — some of them pretty much brand new developers — and one of the first rules I gave them was “ensure that &lt;a class="reference external" href="https://code.visualstudio.com/docs/copilot/overview"&gt;Copilot&lt;/a&gt; (or any other &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Generative_artificial_intelligence"&gt;Generative AI&lt;/a&gt; assistant that will write code for you in your editor) is turned off”. This post explains why. The long and short of it is this: it’s because I want the junior developers to become senior developers, and I want them to enjoy programming as well.&lt;/p&gt;
&lt;p&gt;Other people might also be interested in my reasons, so I’m writing this as a blog post.&lt;/p&gt;
&lt;p&gt;I’ve read many, many other posts that are &lt;a class="reference external" href="https://lucumr.pocoo.org/2025/6/4/changes/"&gt;for&lt;/a&gt; and &lt;a class="reference external" href="https://blog.glyph.im/2025/06/i-think-im-done-thinking-about-genai-for-now.html"&gt;against&lt;/a&gt;, or just plain &lt;a class="reference external" href="https://nolanlawson.com/2025/04/02/ai-ambivalence/"&gt;depressed&lt;/a&gt;, and some of this is about personal preference, but as I’m making rules for other people, I feel I ought to justify those rules just a little.&lt;/p&gt;
&lt;p&gt;I’m also attempting to write this up in a way that hopefully non-programmers can understand. I don’t want to write a whole load of posts about this increasingly tedious subject, so I’m making one slightly broader one that I can just link to if anyone asks my opinion.&lt;/p&gt;
&lt;p&gt;Rather than talk generalities, I’ll build my case using a single very concrete example of real output from an &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Large_language_model"&gt;LLM&lt;/a&gt; on a real task.&lt;/p&gt;
&lt;nav class="contents" id="contents" role="doc-toc"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#the-problem" id="toc-entry-1"&gt;The problem&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#what-i-did" id="toc-entry-2"&gt;What I did&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#how-it-went" id="toc-entry-3"&gt;How it went&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#the-main-point" id="toc-entry-4"&gt;The main point&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#so-what" id="toc-entry-5"&gt;So what?&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#scenario-1" id="toc-entry-6"&gt;Scenario 1&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#scenario-2" id="toc-entry-7"&gt;Scenario 2&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#scenario-3" id="toc-entry-8"&gt;Scenario 3&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#what-about-the-mid-level-programmer" id="toc-entry-9"&gt;What about the mid-level programmer?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#and-what-about-the-senior-programmer" id="toc-entry-10"&gt;And what about the senior programmer?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#updates" id="toc-entry-11"&gt;Updates&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="the-problem"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-1" role="doc-backlink"&gt;The problem&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This example comes from a one-off problem I created when I accidentally ended up with data in two separate &lt;a class="reference external" href="https://www.sqlite.org/"&gt;SQLite&lt;/a&gt; databases, when I wanted it one. The data wasn’t &lt;strong&gt;that&lt;/strong&gt; important – it was all just test data for the project I’m currently working on – so I could have just thrown one of these databases away. But the data had &lt;strong&gt;some&lt;/strong&gt; value to me, so I decided to see if I could use an LLM to quickly create a one-off script to merge the two databases.&lt;/p&gt;
&lt;p&gt;Merging databases sounds like something so common there would be a generic tool to do it, but in reality the devil is in the details, and every &lt;a class="reference external" href="https://www.ibm.com/think/topics/database-schema"&gt;database schema&lt;/a&gt; has specifics that mean you need a custom solution.&lt;/p&gt;
&lt;p&gt;The specific databases in question were pretty small, with a pretty simple schema. The only big problem with the schema, common to many databases schemas, is how you deal with &lt;strong&gt;ID&lt;/strong&gt; fields:&lt;/p&gt;
&lt;p&gt;This database schema has multiple tables, and records in one table are related to records in another table using an ID column, which in my case was just a unique number starting at one: 1, 2, 3 etc. To make it concrete, let’s say my database stored a list of houses, and a list of rooms in each house (it didn’t, but it works as an example).&lt;/p&gt;
&lt;p&gt;So you have a &lt;code class="docutils literal"&gt;houses&lt;/code&gt; table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th class="head"&gt;&lt;p&gt;id&lt;/p&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;p&gt;address&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;45 Main St&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;67 Mayfair&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;3&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;2 Bag End&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;…&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;…&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And a &lt;code class="docutils literal"&gt;rooms&lt;/code&gt; table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th class="head"&gt;&lt;p&gt;id&lt;/p&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;p&gt;house_id&lt;/p&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;p&gt;name&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;Dining room&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;Living room&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;3&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;The Library&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;…&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;…&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;…&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code class="docutils literal"&gt;house_id&lt;/code&gt; column above links each room with a specific house from the &lt;code class="docutils literal"&gt;houses&lt;/code&gt; table. In the example above, the rooms with ids &lt;code class="docutils literal"&gt;1&lt;/code&gt; and &lt;code class="docutils literal"&gt;2&lt;/code&gt; both belong to house with ID &lt;code class="docutils literal"&gt;1&lt;/code&gt;, aka “45 Main St”.&lt;/p&gt;
&lt;p&gt;Due to the database merge, we’ve got multiple instances of each of these tables, and they could very easily be using the same ID values to refer to different things. When I come to merge the tables:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;I’ve got to assign new values in the &lt;code class="docutils literal"&gt;id&lt;/code&gt; columns for each table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;for the &lt;code class="docutils literal"&gt;rooms&lt;/code&gt; table, I’ve got to remember the mapping of old-to-new IDs from the &lt;code class="docutils literal"&gt;houses&lt;/code&gt; table and apply the same mapping to the &lt;code class="docutils literal"&gt;house_id&lt;/code&gt; column.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I get this wrong, the data will be horribly confused and corrupted.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="what-i-did"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-2" role="doc-backlink"&gt;What I did&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I used &lt;a class="reference external" href="https://aider.chat/"&gt;aider.chat&lt;/a&gt;, which is a pretty good project with a good reputation, and one that I’m used to, to the point of being reasonably competent (although I can’t claim I use any of these tools a massive amount). This was a while back, and I can’t remember which LLM model I was using, but I think it was one of the best ones from Claude, or maybe DeepSeek-R1, both of which are (or were) well regarded.&lt;/p&gt;
&lt;p&gt;I fed it the database schema as a SQL file, then prompted it similar to below:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Write a script that takes a list of db files as command line args, and merges the contents. The output will have a single ’metadata’ table copied from one of the input files. The ’rooms’ and ’houses’ tables will be merged, being careful to update ’id’ and ’house_id’ columns correctly, so that they refer to new values.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m not going to spend time arguing about whether this was the best possible tool/model/prompt, that’s an endless time sink – I’m happy that I did a decent enough job on all fronts for my comments to be fair.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="how-it-went"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-3" role="doc-backlink"&gt;How it went&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The results were basically great in most ways that most people would care about: the LLM created a mostly working script of a few hundred lines of code in a few minutes. If I remember correctly it was pretty much there on the first attempt.&lt;/p&gt;
&lt;p&gt;I did actually care about the data, so I carefully checked the script, especially the code that mapped the IDs.&lt;/p&gt;
&lt;p&gt;There was one bit of code that created the mapping, and a second bit of code that then used it. For this second part, correct code would have looked something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_054cacf7909c4f1998a27db4ff51291a-1" name="rest_code_054cacf7909c4f1998a27db4ff51291a-1" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#rest_code_054cacf7909c4f1998a27db4ff51291a-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id_mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;old_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;id_mapping&lt;/code&gt; is a mapping (dictionary) that contains the “old ID” as keys, and the “new ID” as values&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;old_id&lt;/code&gt; is a variable containing an old ID value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the brackets syntax &lt;code class="docutils literal"&gt;[old_id]&lt;/code&gt; does a dictionary lookup and returns the value found – in other words, given the old ID it returns the new ID.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is a crucial question with mappings like this: what happens if, for some reason, the mapping doesn’t contain the &lt;code class="docutils literal"&gt;old_id&lt;/code&gt; value?  With the code as written above, the dictionary lookup will raise an exception, which will cause the code to abort at this point. &lt;strong&gt;This is a good thing&lt;/strong&gt; – since somehow we are missing a value, there is nothing we can do with the current data, and obviously there is some serious problem with our code which means that aborting loudly is the best of our options, or at least the most sensible default.&lt;/p&gt;
&lt;p&gt;However, what the LLM actually wrote was like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_a2ed15c86abe4ebdaa1d21e4ee059d7d-1" name="rest_code_a2ed15c86abe4ebdaa1d21e4ee059d7d-1" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#rest_code_a2ed15c86abe4ebdaa1d21e4ee059d7d-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id_mapping&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This code also does a dictionary lookup, but it uses the &lt;a class="reference external" href="https://docs.python.org/3/library/stdtypes.html#dict.get"&gt;get method&lt;/a&gt; to supply a default value. If the lookup fails because the value is missing, the default is returned instead of raising an exception. The default given here is &lt;code class="docutils literal"&gt;old_id&lt;/code&gt;, which is &lt;strong&gt;the original value&lt;/strong&gt;.  This is, of course, &lt;strong&gt;disastrously wrong&lt;/strong&gt;.  If, for whatever reason, the new ID value is missing, the old ID value is not a good enough fallback – that’s just going to create horribly corrupted data. Worst of all, it will do so absolutely silently – it could just fill the output data with essentially garbage, with no warning that something had gone wrong.&lt;/p&gt;
&lt;p&gt;We might ask where this idea came from — the LLM has written extra code to produce a much worse result, why? The answer is most likely found, in part, in the way the LLM was trained – that is, on mediocre code.&lt;/p&gt;
&lt;p&gt;A better answer to “why” the AI wrote this is much more troubling, but I’ll come back to that.&lt;/p&gt;
&lt;p&gt;We might also ask, “does it matter?”&lt;/p&gt;
&lt;p&gt;Part of the answer to that is context. For the project I’m currently working on, “silently wrong output” is one of the very worst things we can do. There are projects with different priorities, and there are even a few where quality barely matters at all, for which we really wouldn’t care about things like this. There are also lots of projects where you might expect people &lt;strong&gt;would&lt;/strong&gt; care about this, but they actually don’t, which is fairly sad. Anyway, I’m glad not to be currently working on any projects like that – correct output matters, and I like that.&lt;/p&gt;
&lt;p&gt;In this case, there is a second reason you might say it doesn’t matter: the rest of the code was actually correct. This meant that the mapping dictionary was complete, so the incorrect behaviour would never get triggered – the problem I’ve found is actually hypothetical. So what am I worrying about?&lt;/p&gt;
&lt;p&gt;The problem is that in real code, the hypothetical could become reality very quickly. For example:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Some change to the code could introduce a bug which means that the mapping is now missing entries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A refactoring means the code gets used in a slightly different situation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is some change to the context in which the code is used. For example, if other processes were writing to one of these databases while the merge operation was happening, it would be entirely possible for the mapping dictionary to be missing entries.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we find ourselves in an interesting situation:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;The code, as written by the LLM, appears on the one hand to be perfectly adequate, if measured according to the metric of “does it work right now”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the other hand, the code is disastrously inadequate if measured by the standard of letting it go anywhere near important data, or anything you would want to live long term in your code base.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="the-main-point"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-4" role="doc-backlink"&gt;The main point&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Writing correct code is hard. The difference between correct and disastrously bad can be extremely subtle. These differences are easily missed by the untrained eye, and you might not even be able to come up with an example where the bad code fails, because in its initial context it does not.&lt;/p&gt;
&lt;p&gt;There are many, many other examples of this in programming, and there are many examples of LLMs tripping up like this. Often it is not what is present that is even the problem, but what is absent.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="so-what"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-5" role="doc-backlink"&gt;So what?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;OK, so LLMs sometimes write horribly flawed code that appears to work. Couldn’t we say the same about junior programmers?&lt;/p&gt;
&lt;p&gt;Yes, we could. I think the big difference comes when you think about what happens next, after this bad code is written. So, I’ll think about this under 3 scenarios.&lt;/p&gt;
&lt;section id="scenario-1"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-6" role="doc-backlink"&gt;Scenario 1&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In this scenario, I, as a senior developer, am the person who got the LLM to write the code, and I’m now tasked with code review in order to find potential flaws and time-bombs like the above.&lt;/p&gt;
&lt;p&gt;First, in this scenario, the temptation to not check carefully is &lt;strong&gt;very strong&lt;/strong&gt;. The whole reason I’m tempted to use an LLM in the first place is that I don’t want to devote much time to the task. For me this happens when:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;I can’t &lt;strong&gt;justify&lt;/strong&gt; much time, because I consider it’s not that important - it’s just something I need to do to get back to the main thing I’m doing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I don’t think I will &lt;strong&gt;enjoy&lt;/strong&gt; spending that time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For both of these cases, the “must go faster” mindset means it’s psychologically very hard to slow down and do the careful review needed.&lt;/p&gt;
&lt;p&gt;So, I’m unlikely to review this code as carefully as I should. For me, assuming that the code matters at all, this is a killer problem.&lt;/p&gt;
&lt;p&gt;Maybe someone else would review it and catch this? That’s not good enough for me – &lt;strong&gt;I don’t rely on other people reviewing my code&lt;/strong&gt;. I’m a professional software developer who works on important projects. Sometimes I work alone, with no-one else doing effective review. My clients expect and deserve that I write code that actually works.&lt;/p&gt;
&lt;p&gt;Of course I also know that I’m far from perfect, and welcome any code review I can get. But even when I think there is review going on, I treat it as a backup, an extra safety measure, and not a reason to justify being sloppy and careless.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="scenario-2"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-7" role="doc-backlink"&gt;Scenario 2&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In this scenario, the code was written by a junior developer. In contrast to the previous section, I don’t expect junior developers to produce code that works. But I do expect them to &lt;strong&gt;learn&lt;/strong&gt; to do so. And I hope that that I will be rightly suspicious and do a thorough review.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://blog.glyph.im/2025/06/i-think-im-done-thinking-about-genai-for-now.html"&gt;Like Glyph&lt;/a&gt;, I actually quite enjoy doing code review for junior developers, as long as they actually have both the willingness and capacity to improve (which they usually do, but not always). Code review can be a great opportunity to actually train someone.&lt;/p&gt;
&lt;p&gt;So what happens in this scenario when I, hopefully, after careful review spot the flaw and point it out?&lt;/p&gt;
&lt;p&gt;If I have time to properly mentor a developer, the process would be a conversation (preferably in person or a video call) that starts something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When you wrote &lt;code class="docutils literal"&gt;new_id = old_id_to_new_id.get(old_id, old_id)&lt;/code&gt;, can you explain &lt;strong&gt;what was going through your mind&lt;/strong&gt;? Why did you choose to write &lt;code class="docutils literal"&gt;.get(old_id, old_id)&lt;/code&gt;, rather than the simpler and shorter &lt;code class="docutils literal"&gt;[old_id]&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It’s possible the reason was them thinking “doing something is better than crashing”. We can then address that disastrously wrong (but quite common) mindset. This will hopefully be a very fruitful discussion, because it will actually correct the issue &lt;strong&gt;at the root&lt;/strong&gt;, and so stop it happening again, or at least make it much less likely.&lt;/p&gt;
&lt;p&gt;In some cases, there isn’t a reason “why” they wrote the code they did –  sometimes junior developers just don’t understand a lot of the code they are writing, they are just guessing until something seems to work. In that case, we can address that problem:&lt;/p&gt;
&lt;p&gt;The developer needs to go back to basics of things like assignments and function calls etc. The developer must reach the point where they can explain and justify every last jot and tittle of code they produce. It seems pretty obvious to me that the best way to achieve that is to make them write pretty much all their code, at least at the level of choosing each “token” that they add (e.g. choosing from a list of methods with auto-complete is OK, but not more than that). If I want them to be able to justify each token choice, it is essential to make them engage their brain and choose each token.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="scenario-3"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-8" role="doc-backlink"&gt;Scenario 3&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The third scenario is again that the junior developer “produced” the code, and I’m now reviewing it, but it turns out they just prompted some LLM assistant.&lt;/p&gt;
&lt;p&gt;At this point, the first step in the root cause analysis – the question “what was your thought process in producing this code” – fails immediately.&lt;/p&gt;
&lt;p&gt;There is no real answer to the question “why” the LLM wrote the bad code in the first place. You can’t ask it “what were you were thinking” and get a meaningful answer – &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/chatgpt-no-inner-monologue-or-meta-cognition/"&gt;it’s pointless to ask so don’t even try&lt;/a&gt;. It lacks the meta-cognition needed for self-improvement, and much of the time &lt;a class="reference external" href="https://machinelearning.apple.com/research/illusion-of-thinking"&gt;when it looks like it is reasoning, it is actually just pattern matching&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The next problem is that the LLM basically can’t learn, at least not deeply enough to make a significant difference. You can tell it “don’t do that again”, and it might partly work, but you can’t really change how it thinks, just what the current instructions are.&lt;/p&gt;
&lt;p&gt;So we can’t address the root cause.&lt;/p&gt;
&lt;p&gt;What about the junior developer learning from this, in this scenario – is there something they could take away which would prevent the mistake in the future?&lt;/p&gt;
&lt;p&gt;If their own mind wasn’t engaged in making the mistake, I think it is quite unlikely that they will be able to effectively learn from the LLM’s mistakes. For the junior dev, the possible take-aways about changes to &lt;strong&gt;their own behaviour&lt;/strong&gt; are:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Check the LLM’s output more carefully. But I’m not convinced they are equipped at this point to really do checking – they first need practice in the careful thinking about what correct code looks like, to gain the trained eye that can spot subtle issues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don’t use LLMs to write code (which is what I’m arguing here)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="what-about-the-mid-level-programmer"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-9" role="doc-backlink"&gt;What about the mid-level programmer?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At what point do I suggest the junior developers should start using LLMs for some of the more boring tasks? Once they are “mid-level” (whatever that means), is it appropriate?&lt;/p&gt;
&lt;p&gt;There is obviously a point at which you let people make their own decisions.&lt;/p&gt;
&lt;p&gt;For myself, I’m pretty firmly convinced that the software I’ve just recently
created with 25+ years’ professional experience, I simply couldn’t have created
with only 15 years experience. Not because I’m a poor programmer – I think I’ve got good reason to believe I’m significantly above average (for example, I taught myself machine code and assembly as a young teenager, and I’ve been part of the core team of &lt;a class="reference external" href="https://www.djangoproject.com/"&gt;top open-source projects&lt;/a&gt;). There is just a lot to learn, and always a
lot further you could go.&lt;/p&gt;
&lt;p&gt;In this most recent project, we’ve seen a lot of success because of certain key architectural decisions that I made right at the beginning. Some of these used advanced patterns that 10 years I would not have instinctively reached for, or wouldn’t even have known well enough to attempt them.&lt;/p&gt;
&lt;p&gt;For example, due to difficulties with manual testing of the output in my
current project, we depend critically on regression tests that make heavy use of
a version of &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Command_pattern"&gt;the Command pattern&lt;/a&gt; or &lt;a class="reference external" href="https://mmapped.blog/posts/29-plan-execute"&gt;plan-execute pattern&lt;/a&gt;. In fact we use multiple
levels of it, one level being essentially an embedded DSL that has both
an evaluator and compiler. This code is not trivial, and it’s also quite far
from the simplest thing that you would think of, but it has proven critical to
success.&lt;/p&gt;
&lt;p&gt;How did I know I needed these? Well, I can tell you one thing for sure: I would never have got the experience and judgement necessary if I hadn’t been doing a lot of the coding myself over the past 25 years.&lt;/p&gt;
&lt;p&gt;In his video &lt;a class="reference external" href="https://www.youtube.com/watch?v=ltLUadnCyi0"&gt;A tale of two problem solvers&lt;/a&gt;, youtuber &lt;a class="reference external" href="https://www.youtube.com/@3blue1brown"&gt;3Blue1Brown&lt;/a&gt; has a section that is hugely relevant here, especially from 36 minutes onwards. In it, he describes how practising mathematicians will often do hundreds of concrete examples in order to build up intuition and sharpen their skills so that they can tackle more general and harder problems. What particularly struck me was that famous mathematicians throughout history have done this – “they all have this seemingly infinite patience for doing tedious calculations”.&lt;/p&gt;
&lt;p&gt;Computer programming may seem different, in that we deliberately don’t do the tedious calculations, but teach the computer to do that. However, there are huge similarities. Programming, like mathematics, involves a &lt;strong&gt;formal notation&lt;/strong&gt; and &lt;strong&gt;problem solving&lt;/strong&gt;. In the case pf programming, the formal notation is aimed at a machine that will mechanically interpret it to produce a desired behaviour.&lt;/p&gt;
&lt;p&gt;Obviously we do avoid doing lots of long multiplication once we’ve taught the computer to do that. However, given the similarities of the mental processes involved in maths and programming, when it comes to any of the higher level things about how to structure programs, I think we are absolutely fooling ourselves if we think we can avoid doing all the “grunt work” of writing code, organising code bases, slowly improving code structure, etc. and still end up magically knowing all the patterns that we need to use, understanding their trade-offs, and all the reasons why certain architectures or patterns would or wouldn’t be appropriate.&lt;/p&gt;
&lt;p&gt;If you ever want to progress beyond mid-level, I strongly suspect that offloading significant parts of programming to an LLM will greatly reduce your growth. You may be much &lt;strong&gt;faster&lt;/strong&gt; at outputting things of a similar level to what you can currently handle, but I doubt you’ll be able to tackle fundamentally harder projects in the future.&lt;/p&gt;
&lt;p&gt;While I’m talking about youtubers, the video &lt;a class="reference external" href="https://www.youtube.com/watch?v=5eW6Eagr9XA"&gt;The Expert Myth&lt;/a&gt; by &lt;a class="reference external" href="https://www.youtube.com/@veritasium"&gt;Veritasium&lt;/a&gt; is also really helpful here. He describes how expertise requires the following ingredients:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;p&gt;Valid environment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Many repetitions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Timely feedback&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deliberate practice&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As far as I can see, heavy use of LLMs to write code for you will destroy points 2 and 3 – neither the repetitions nor the feedback are really happening if you don’t actually write the code yourself. I doubt that the “deliberate practice and study, in an uncomfortable zone” is going to happen either if you never get happy with the manual bits of coding.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="and-what-about-the-senior-programmer"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-10" role="doc-backlink"&gt;And what about the senior programmer?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To complete this post, we of course want to ask, should the “senior programmer” use LLMs to do a lot of the grunt programming? By senior, I do mean significantly more than the 5 years that many “seniors” have. Does there come a point where you have “arrived” and the arguments in the previous section no longer apply? A time when you basically don’t need to do much more learning and sharpening of skills?&lt;/p&gt;
&lt;p&gt;My answer to that is “I hope not”. Like others, I’m still hoping that by the end of my career I could be at least &lt;a class="reference external" href="https://www.scattered-thoughts.net/writing/speed-matters/"&gt;10x faster&lt;/a&gt; than I am now (without an LLM writing the code for me), maybe even more. Computers are mental levers, so I don’t think that’s ridiculous. I’m hoping not just to be 10x faster, but 10x better in other ways – in terms of reliability, or in terms of being able to tackle problems that would just stump me today, or to which my solutions today would be massively inferior.&lt;/p&gt;
&lt;p&gt;Everything I know about learning says that outsourcing my actual thinking to LLMs is unlikely to produce that result.&lt;/p&gt;
&lt;p&gt;In addition, there are at least some people who, after actively using LLMs integrated into their editor (like Copilot and Cursor), &lt;a class="reference external" href="https://lucianonooijen.com/blog/why-i-stopped-using-ai-code-editors/"&gt;have now stopped doing so because they noticed their skills were rusting&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;These arguments, plus the small amount of evidence I have, are enough that I don’t feel the need to make a guinea pig out of myself to collect more data.&lt;/p&gt;
&lt;p&gt;So, for myself, I choose to use LLMs &lt;strong&gt;very rarely&lt;/strong&gt; for actually writing code. Rarely enough that if they were to disappear completely, it would make little difference to me.&lt;/p&gt;
&lt;p&gt;But, aside from the practical arguments regarding becoming a better programmer, one of the big reasons for this is that I &lt;strong&gt;simply enjoy programming&lt;/strong&gt;.  I enjoy the challenge and the process of getting the computer to do exactly what I want it to do, in a reasonable amount of time, expressing myself both precisely and concisely.&lt;/p&gt;
&lt;p&gt;When you are programming at the optimal level of abstraction, it is actually &lt;strong&gt;much nicer&lt;/strong&gt; to express yourself in code compared to English, and often much faster too. Natural language is truly horrible for times when you want precision. And in computer programming, you usually are able to &lt;strong&gt;create the abstractions you need&lt;/strong&gt;, so you can often get close to that optimal level of abstraction.&lt;/p&gt;
&lt;p&gt;Obviously there are times when there is a bad fit between the abstractions you want and the abstractions you have, resulting in a lot of redundancy. You can’t always rewrite everything to make it all as ideal as you want. But if I’m writing large amounts of code at the wrong abstraction level, that’s bad, and I don’t really want a system that helps me to write more and more code like that.&lt;/p&gt;
&lt;p&gt;The point of this section is really for the benefit of the junior developers that I’m forcing to do things “the hard way”. What I’m saying is this: I’m not withholding some special treat that I reserve just for myself. I willingly code in exactly the same way as you, and I really enjoy it. I believe that I’m sparing you the miserable existence of never becoming good at programming, while you keep trying to cajole something that doesn’t understand what it’s doing into producing something you don’t understand either. That just doesn’t sound like my idea of fun.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="updates"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/why-im-not-letting-the-juniors-use-genai-for-coding/#toc-entry-11" role="doc-backlink"&gt;Updates&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;March 2026:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It looks like Carson Gross, of whom I’m a big fan, &lt;a class="reference external" href="https://htmx.org/essays/yes-and/"&gt;agrees with this stance when it comes to his students&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since writing the above, I have successfully used coding agents to produce &lt;a class="reference external" href="https://github.com/spookylukey/planegcs/"&gt;significant, high quality projects&lt;/a&gt; where I have done extremely little of the actual coding. However, I stand by what I’ve written above: while the agent was coding away, I never relegated myself to merely managing it. I was coding away just as hard, but on a different, related bit of work. I still don’t use AI integrated into my editor.&lt;/p&gt;
&lt;p&gt;I’m not letting my skills stagnate, and if I get an agent to work outside my area of competency, I do it consciously. For example, in the planegcs library linked above, I was quite happy to not improve my understanding of C++ and its build tools. On the other hand, I think I’m going to need to do a Rust project, and I’m intending to do all the coding myself, even though I know an agent could make much more rapid progress, because I actually want to get good at Rust.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</content>
    <category term="artificial-intelligence" label="Artificial Intelligence"/>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
  </entry>
  <entry>
    <title>Statically checking Python dicts for completeness</title>
    <id>https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/</id>
    <updated>2025-06-27T11:09:03+01:00</updated>
    <published>2025-06-27T11:09:03+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/"/>
    <summary type="html">&lt;p&gt;A Pythonic way to ensure that your statically-defined dicts are complete, with full source code.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In Python, I often have the situation where I create a dictionary, and want to ensure that it is &lt;strong&gt;complete&lt;/strong&gt; – it has an entry for every valid key.&lt;/p&gt;
&lt;p&gt;Let’s say for my (currently hypothetical) automatic squirrel-deterring water gun system, I have a number of different states the water tank can be in, defined using an enum:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-1" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StrEnum&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-2" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-3" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TankState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StrEnum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-4" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;FULL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FULL"&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-5" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;HALF_FULL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HALF_FULL"&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-6" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;NEARLY_EMPTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NEARLY_EMPTY"&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-7" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-7" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-7"&gt;&lt;/a&gt;    &lt;span class="n"&gt;EMPTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EMPTY"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In a separate bit of code, I define an RGB colour for each of these states, using a simple dict.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-1" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-2" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x00FF00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-3" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HALF_FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x28D728&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-4" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NEARLY_EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF9900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-5" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-6" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-6"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is deliberately distinct from my &lt;code class="docutils literal"&gt;TankState&lt;/code&gt; code and related definitions, because it relates to a different part of the project - the user interface. The UI concerns shouldn’t be mixed up with the core logic.&lt;/p&gt;
&lt;p&gt;This dict is fine, and currently complete. But I’d like to ensure that if I add a new item to &lt;code class="docutils literal"&gt;TankState&lt;/code&gt;, I don’t forget to update the &lt;code class="docutils literal"&gt;TANK_STATE_COLORS&lt;/code&gt; dict.&lt;/p&gt;
&lt;p&gt;With a growing ability to do static type checks in Python, &lt;a class="reference external" href="https://stackoverflow.com/questions/72022403/type-hint-for-an-exhaustive-dictionary-with-enum-literal-keys"&gt;some people have asked how we can ensure this using static type checks&lt;/a&gt;. The short answer is, we can’t (at least at the moment).&lt;/p&gt;
&lt;p&gt;But the better question is “how can we (somehow) ensure we don’t forget?” It doesn’t have to be a static type check, as long as it’s very hard to forget, and if it preferably runs as early as possible.&lt;/p&gt;
&lt;p&gt;Instead of shoe-horning everything into static type checks, let’s just make use of the fact that this is Python and we can write any code we want at module level. All we need to do is this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-1" name="rest_code_ebc769b710ff414a828735cf76d69018-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-2" name="rest_code_ebc769b710ff414a828735cf76d69018-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-2"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-3" name="rest_code_ebc769b710ff414a828735cf76d69018-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-4" name="rest_code_ebc769b710ff414a828735cf76d69018-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-5" name="rest_code_ebc769b710ff414a828735cf76d69018-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-5"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"TANK_STATE_COLORS is missing an entry for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s it, that’s the whole technique. I’d argue that this is a pretty much optimal, Pythonic solution to the problem. No clever type tricks to debug later, just 2 lines of plain simple code, and it’s impossible to import your code until you fix the problem, which means you get the early checking you want.
Plus you get &lt;strong&gt;exactly the error message you want&lt;/strong&gt;, not some obscure compiler output, which is also really important.&lt;/p&gt;
&lt;p&gt;It can also be extended if you want to do something more fancy (e.g. allow some values of the enum to be missing), and if it does get in your way, you can turn it off temporarily by just commenting out a couple of lines.&lt;/p&gt;
&lt;section id="thats-not-quite-it"&gt;
&lt;h2&gt;That’s not quite it&lt;/h2&gt;
&lt;p&gt;OK, in a project where I’m using this &lt;strong&gt;a lot&lt;/strong&gt;, I did eventually get bored of this small bit of boilerplate. So, as a Pythonic extension of this Pythonic solution, I now do this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-1" name="rest_code_92e7c9499aba47fbac2310608a002230-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&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="n"&gt;TankState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-2" name="rest_code_92e7c9499aba47fbac2310608a002230-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x00FF00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-3" name="rest_code_92e7c9499aba47fbac2310608a002230-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HALF_FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x28D728&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-4" name="rest_code_92e7c9499aba47fbac2310608a002230-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NEARLY_EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF9900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-5" name="rest_code_92e7c9499aba47fbac2310608a002230-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-6" name="rest_code_92e7c9499aba47fbac2310608a002230-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-6"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-7" name="rest_code_92e7c9499aba47fbac2310608a002230-7" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-7"&gt;&lt;/a&gt;&lt;span class="n"&gt;assert_complete_enumerations_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Specifically, I’m adding:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;a type hint on the constant&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a call to a clever utility function that does just the right amount of Python magic.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This function needs to be “magical” because we want it to produce good error messages, like we had before. This means it needs to get hold of the name of the dict in the calling module, but functions don’t usually have access to that.&lt;/p&gt;
&lt;p&gt;In addition, it wants to get hold of the type hint (although there would be other ways to infer it without a type hint, there are advantages this way), for which we also need the name.&lt;/p&gt;
&lt;p&gt;The specific magic we need is:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;the clever function needs to get hold of the module that &lt;strong&gt;called it&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it then looks through the module dictionary to get the name of the object that has been passed in&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then it can find type hints, and do the checking.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, because you don’t want to write all that yourself, the code is below. It also supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;having a tuple of &lt;code class="docutils literal"&gt;Enum&lt;/code&gt; types as the key&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;allowing some items to be missing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;using &lt;code class="docutils literal"&gt;Literal&lt;/code&gt; as the key. So you can do things like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_43dffe504c24474bae463e035898536f-1" name="rest_code_43dffe504c24474bae463e035898536f-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;X&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="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-2" name="rest_code_43dffe504c24474bae463e035898536f-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"negative"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-3" name="rest_code_43dffe504c24474bae463e035898536f-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-3"&gt;&lt;/a&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"zero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-4" name="rest_code_43dffe504c24474bae463e035898536f-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-4"&gt;&lt;/a&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"positive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-5" name="rest_code_43dffe504c24474bae463e035898536f-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-6" name="rest_code_43dffe504c24474bae463e035898536f-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-6"&gt;&lt;/a&gt;&lt;span class="n"&gt;assert_complete_enumerations_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s got a ton of error checking, because once you get magical then you really don’t want to be debugging obscure messages.&lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;
&lt;p&gt;I hereby place the following code into the public domain - &lt;a class="reference external" href="https://creativecommons.org/publicdomain/zero/1.0/"&gt;CC0 1.0 Universal&lt;/a&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-1" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;inspect&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-2" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;itertools&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-3" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-4" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-4"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-5" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-5"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-6" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-6"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-7" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-7" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-8" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-8" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-8"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;frozendict&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;frozendict&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-9" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-9" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-9"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-10" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-10" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-10"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-11" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-11" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-11"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_complete_enumerations_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()):&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-12" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-12" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-12"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-13" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-13" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-13"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Magically assert that the dict in the calling module has a&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-14" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-14" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-14"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    value for every item in an enumeration.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-15" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-15" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-15"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    The dict object must be bound to a name in the module.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-16" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-16" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-16"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    It must be type hinted, with the key being an Enum subclass, or Literal.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-17" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-17" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-17"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    The key may also be a tuple of Enum subclasses&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-18" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-18" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-18"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-19" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-19" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-19"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    If you expect some values to be missing, pass them in `allowed_missing`&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-20" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-20" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-20"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-21" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-21" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-21"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt; is not a dict or mapping, it is a &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-22" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-22" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-22"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-23" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-23" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-23"&gt;&lt;/a&gt;    &lt;span class="n"&gt;frame_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_getframe&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="c1"&gt;# type: ignore[reportPrivateUsage]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-24" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-24" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-24"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;frame_up&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-25" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-25" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-25"&gt;&lt;/a&gt;    &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getmodule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame_up&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-26" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-26" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-26"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Couldn't get module for frame &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;frame_up&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-27" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-27" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-27"&gt;&lt;/a&gt;    &lt;span class="n"&gt;msg_prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"In module `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`,"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-28" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-28" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-28"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-29" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-29" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-29"&gt;&lt;/a&gt;    &lt;span class="n"&gt;module_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame_up&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;f_locals&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-30" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-30" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-30"&gt;&lt;/a&gt;    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-31" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-31" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-31"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# Find the object:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-32" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-32" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-32"&gt;&lt;/a&gt;    &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;module_dict&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&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;val&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-33" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-33" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-33"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; there is no name for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, please check"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-34" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-34" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-34"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-35" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-35" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-35"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# Any name that has a type hint will do, there will usually be one.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-36" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-36" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-36"&gt;&lt;/a&gt;    &lt;span class="n"&gt;hints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_type_hints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-37" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-37" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-37"&gt;&lt;/a&gt;    &lt;span class="n"&gt;hinted_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hints&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-38" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-38" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-38"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-39" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-39" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-39"&gt;&lt;/a&gt;        &lt;span class="n"&gt;hinted_names&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-40" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-40" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-40"&gt;&lt;/a&gt;    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; no type hints were found for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, they are needed to use assert_complete_enumerations_dict"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-41" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-41" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-41"&gt;&lt;/a&gt;    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hinted_names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-42" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-42" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-42"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-43" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-43" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-43"&gt;&lt;/a&gt;    &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hints&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-44" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-44" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-44"&gt;&lt;/a&gt;    &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-45" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-45" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-45"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must supply arguments"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-46" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-46" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-46"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-47" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-47" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-47"&gt;&lt;/a&gt;        &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-48" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-48" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-48"&gt;&lt;/a&gt;        &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-49" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-49" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-49"&gt;&lt;/a&gt;        &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-50" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-50" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-50"&gt;&lt;/a&gt;        &lt;span class="n"&gt;frozendict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-51" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-51" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-51"&gt;&lt;/a&gt;    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must be dict/frozendict/Mapping with arguments to use assert_complete_enumerations_dict, not &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-52" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-52" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-52"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-53" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-53" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-53"&gt;&lt;/a&gt;    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-54" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-54" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-54"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must have two args"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-55" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-55" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-55"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-56" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-56" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-56"&gt;&lt;/a&gt;    &lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-57" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-57" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-57"&gt;&lt;/a&gt;    &lt;span class="n"&gt;arg0_origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-58" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-58" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-58"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arg0_origin&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-59" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-59" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-59"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# tuple of Enums&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-60" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-60" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-60"&gt;&lt;/a&gt;        &lt;span class="n"&gt;enum_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-61" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-61" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-61"&gt;&lt;/a&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;enum_cls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enum_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-62" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-62" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-62"&gt;&lt;/a&gt;            &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;issubclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-63" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-63" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-63"&gt;&lt;/a&gt;                &lt;span class="n"&gt;enum_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-64" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-64" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-64"&gt;&lt;/a&gt;            &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint must be an Enum to use assert_complete_enumerations_dict, not &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;enum_cls&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-65" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-65" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-65"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-66" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-66" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-66"&gt;&lt;/a&gt;        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enum_cls&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;enum_cls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enum_list&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-67" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-67" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-67"&gt;&lt;/a&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;arg0_origin&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-68" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-68" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-68"&gt;&lt;/a&gt;        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-69" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-69" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-69"&gt;&lt;/a&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-70" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-70" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-70"&gt;&lt;/a&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;issubclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-71" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-71" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-71"&gt;&lt;/a&gt;            &lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-72" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-72" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-72"&gt;&lt;/a&gt;        &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint must be an Enum to use assert_complete_enumerations_dict, not &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-73" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-73" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-73"&gt;&lt;/a&gt;        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-74" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-74" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-74"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-75" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-75" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-75"&gt;&lt;/a&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-76" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-76" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-76"&gt;&lt;/a&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-77" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-77" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-77"&gt;&lt;/a&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-78" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-78" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-78"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-79" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-79" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-79"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# This is the assert we actually want to do, everything else is just error checking:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-80" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-80" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-80"&gt;&lt;/a&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; needs an entry for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;</content>
    <category term="python" label="Python"/>
    <category term="python-type-hints" label="Python type hints"/>
  </entry>
  <entry>
    <title>Knowledge creates technical debt</title>
    <id>https://lukeplant.me.uk/blog/posts/knowledge-creates-technical-debt/</id>
    <updated>2025-05-13T09:08:50+01:00</updated>
    <published>2025-05-13T09:08:50+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/knowledge-creates-technical-debt/"/>
    <summary type="html">&lt;p&gt;Some history on term “technical debt” and on better language to use when communicating about it.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;The term &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Technical_debt"&gt;technical debt&lt;/a&gt;, now used widely in software circles, &lt;a class="reference external" href="https://www.youtube.com/watch?v=pqeJFYwnkjE"&gt;was coined to explain a deliberate process where you write software quickly to gain knowledge&lt;/a&gt;, and then you have to use that knowledge gained to improve your software.&lt;/p&gt;
&lt;p&gt;This perspective is still helpful today when people speak of technical debt as only a negative, or only as a result of bad decisions. Martin Fowler’s &lt;a class="reference external" href="https://martinfowler.com/bliki/TechnicalDebtQuadrant.html"&gt;Tech Debt Quadrant&lt;/a&gt; is a useful antidote to that.&lt;/p&gt;
&lt;p&gt;A consequence of this perspective is that technical debt can appear at any time, apparently from nowhere, if you are unfortunate enough to gain some knowledge.&lt;/p&gt;
&lt;p&gt;If you discover a better way to do things, the old way of doing it that is embedded in your code base is now “debt”:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;you can either live with the debt, “paying interest” in the form of all the ways that it makes your code harder to work with;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or you can “pay down” the debt by fixing all the code in light of your new knowledge, which takes up front resources which could have been spent on something else, but hopefully will make sense in the long term.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This “better way” might be a different language, library, tool or pattern. In some cases, the better way has only recently been invented. It might be your own personal discovery, or something industry wide. It might be knowledge gained through the actual work of doing the current project (which was Ward Cunningham’s usage of the tem), or from somewhere else. But the end result is the same – you know more than you did, and now you have a debt.&lt;/p&gt;
&lt;p&gt;The problem is that this doesn’t sound like a good thing. You learn something, and now you have a problem you didn’t have before, and it’s difficult to put a good spin on “I discovered a debt”.&lt;/p&gt;
&lt;p&gt;But from another angle, maybe this perspective gives us different language to use when communicating with others and explaining why we need to address technical debt. Rather than say “we have a liability”, the knowledge we have gained can be framed as an opportunity. Failure to take the opportunity is an opportunity cost.&lt;/p&gt;
&lt;p&gt;The “pile of technical debt” is essentially a pile of &lt;strong&gt;knowledge&lt;/strong&gt; – everything we now think is bad about the code represents what we’ve learned about how to do software better. The gap between what it is and what it should be is the gap between what we used to know and what we now know.&lt;/p&gt;
&lt;p&gt;And fixing that code is not “a debt we have to pay off”, but an investment opportunity that will reap rewards. You can refuse to take that opportunity if you want, but it’s a tragic waste of your hard-earned knowledge – a waste of the investment you previously made in learning – and eventually you’ll be losing money, and losing out to competitors who will be making the most of their knowledge.&lt;/p&gt;
&lt;p&gt;Finally, I think phrasing it in terms of knowledge can help tame some of our more rash instincts to call everything we don’t like “tech debt”. Can I really say “we now &lt;strong&gt;know&lt;/strong&gt;” that the existing code is inferior? Is it true that fixing the code is “investing my knowledge”? If it’s just a hunch, or a personal preference, or the latest fashion, maybe I can both resist the urge for unnecessary rewrites, and feel happier about it at the same time.&lt;/p&gt;
&lt;section id="links"&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://lobste.rs/s/kexbxy/knowledge_creates_technical_debt"&gt;Discussion of this post on Lobsters&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</content>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
  </entry>
  <entry>
    <title>Christ the True and Better Frodo</title>
    <id>https://lukeplant.me.uk/blog/posts/christ-the-true-and-better-frodo/</id>
    <updated>2025-02-12T20:44:13Z</updated>
    <published>2025-02-12T20:44:13Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/christ-the-true-and-better-frodo/"/>
    <summary type="html">&lt;p&gt;I got ChatGPT to rewrite “Christ the True and Better Adam” with Lord of the Rings characters, I think it did pretty good.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Inspired by our family conversation at our meal time tonight — we have three boys who are already big Lord of the Rings fans — I decided to put ChatGPT to some edifying use.&lt;/p&gt;
&lt;div class="chatgpt-me docutils container"&gt;
&lt;p&gt;I want you to rewrite the following song, &lt;a class="reference external" href="https://www.mattpapa.com/songs/christ-the-true-and-better"&gt;Christ the True and Better Adam&lt;/a&gt;, by Matt Boswell / Matt Papa / Keith Getty, which compares Jesus to Old Testament figures. I want a new version which compares instead to characters from Lord of the Rings. It should include references to the different things they did, and how Christ is the better version of them. It's important to keep the lyrics in the same meter, so it could be sung to the original tune.  Here is the original song:&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Christ, the true and better Adam&lt;/div&gt;
&lt;div class="line"&gt;Son of God and Son of man&lt;/div&gt;
&lt;div class="line"&gt;Who, when tempted in the garden&lt;/div&gt;
&lt;div class="line"&gt;Never yielded, never sinned&lt;/div&gt;
&lt;div class="line"&gt;He, who makes the many righteous&lt;/div&gt;
&lt;div class="line"&gt;Brings us back to life again&lt;/div&gt;
&lt;div class="line"&gt;Dying, He reversed the curse, then&lt;/div&gt;
&lt;div class="line"&gt;Rising, crushed the serpent's head&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Christ, the true and better Isaac&lt;/div&gt;
&lt;div class="line"&gt;Humble son of sacrifice&lt;/div&gt;
&lt;div class="line"&gt;Who would climb the fearful mountain&lt;/div&gt;
&lt;div class="line"&gt;There to offer up his life&lt;/div&gt;
&lt;div class="line"&gt;Laid with faith upon the altar&lt;/div&gt;
&lt;div class="line"&gt;Father's joy and only son&lt;/div&gt;
&lt;div class="line"&gt;There salvation was provided&lt;/div&gt;
&lt;div class="line"&gt;Oh, what full and boundless love&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Amen! Amen!&lt;/div&gt;
&lt;div class="line"&gt;From beginning to end&lt;/div&gt;
&lt;div class="line"&gt;Christ the story, His the glory&lt;/div&gt;
&lt;div class="line"&gt;Alleluia! Amen!&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Christ, the true and better Moses&lt;/div&gt;
&lt;div class="line"&gt;Called to lead a people home&lt;/div&gt;
&lt;div class="line"&gt;Standing bold to earthly powers&lt;/div&gt;
&lt;div class="line"&gt;God's great glory to be known&lt;/div&gt;
&lt;div class="line"&gt;With his arms stretched wide to heaven&lt;/div&gt;
&lt;div class="line"&gt;See the waters part in two&lt;/div&gt;
&lt;div class="line"&gt;See the veil is torn forever&lt;/div&gt;
&lt;div class="line"&gt;Cleansed with blood we pass now through&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Amen! Amen!&lt;/div&gt;
&lt;div class="line"&gt;From beginning to end&lt;/div&gt;
&lt;div class="line"&gt;Christ the story, His the glory&lt;/div&gt;
&lt;div class="line"&gt;Alleluia! Amen!&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Christ, the true and better David&lt;/div&gt;
&lt;div class="line"&gt;Lowly shepherd, mighty king&lt;/div&gt;
&lt;div class="line"&gt;He the champion in the battle&lt;/div&gt;
&lt;div class="line"&gt;Where, o death, is now thy sting?&lt;/div&gt;
&lt;div class="line"&gt;In our place He bled and conquered&lt;/div&gt;
&lt;div class="line"&gt;Crown Him Lord of majesty&lt;/div&gt;
&lt;div class="line"&gt;His shall be the throne forever&lt;/div&gt;
&lt;div class="line"&gt;We shall e'er His people be&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Amen! Amen!&lt;/div&gt;
&lt;div class="line"&gt;From beginning to end&lt;/div&gt;
&lt;div class="line"&gt;Christ the story, His the glory&lt;/div&gt;
&lt;div class="line"&gt;Alleluia! Amen!&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="chatgpt-it docutils container"&gt;
&lt;p&gt;Here's your rewritten version comparing Christ to characters from The Lord of the Rings, keeping the same meter so it can be sung to the original tune.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Christ, the true and better Frodo&lt;/div&gt;
&lt;div class="line"&gt;Bore the burden none could bear&lt;/div&gt;
&lt;div class="line"&gt;Left His home to face the darkness&lt;/div&gt;
&lt;div class="line"&gt;Took the cross to end despair&lt;/div&gt;
&lt;div class="line"&gt;Through the fire of tribulation&lt;/div&gt;
&lt;div class="line"&gt;Passed alone to win the fight&lt;/div&gt;
&lt;div class="line"&gt;Crushed the power of the darkness&lt;/div&gt;
&lt;div class="line"&gt;Bringing us into the light&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Christ, the true and better Aragorn&lt;/div&gt;
&lt;div class="line"&gt;He, the long-expected King&lt;/div&gt;
&lt;div class="line"&gt;Left His throne to seek the hopeless&lt;/div&gt;
&lt;div class="line"&gt;Now we rise at His redeeming&lt;/div&gt;
&lt;div class="line"&gt;From the paths of death He called us&lt;/div&gt;
&lt;div class="line"&gt;Broke the curse, renewed the land&lt;/div&gt;
&lt;div class="line"&gt;By His hand we reign in glory&lt;/div&gt;
&lt;div class="line"&gt;In His kingdom e'er to stand&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Amen! Amen!&lt;/div&gt;
&lt;div class="line"&gt;From beginning to end&lt;/div&gt;
&lt;div class="line"&gt;Christ the story, His the glory&lt;/div&gt;
&lt;div class="line"&gt;Alleluia! Amen!&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Christ, the true and better Gandalf&lt;/div&gt;
&lt;div class="line"&gt;Came to break the prison's seal&lt;/div&gt;
&lt;div class="line"&gt;Faced the shadow, fell in darkness&lt;/div&gt;
&lt;div class="line"&gt;Rose again, the doom repealed&lt;/div&gt;
&lt;div class="line"&gt;Leading forth a ransomed people&lt;/div&gt;
&lt;div class="line"&gt;Bringing hope where all seemed lost&lt;/div&gt;
&lt;div class="line"&gt;By His wisdom, by His power&lt;/div&gt;
&lt;div class="line"&gt;Every fear and foe is crushed&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Christ, the true and better Samwise&lt;/div&gt;
&lt;div class="line"&gt;Faithful till the journey’s end&lt;/div&gt;
&lt;div class="line"&gt;Took our burden, bore it for us&lt;/div&gt;
&lt;div class="line"&gt;Loved beyond what we could send&lt;/div&gt;
&lt;div class="line"&gt;At our weakest, He sustains us&lt;/div&gt;
&lt;div class="line"&gt;Never turning from the way&lt;/div&gt;
&lt;div class="line"&gt;At His side we reach the homeland&lt;/div&gt;
&lt;div class="line"&gt;Ever in His light to stay&lt;/div&gt;
&lt;div class="line"&gt;&lt;br&gt;&lt;/div&gt;
&lt;div class="line"&gt;Amen! Amen!&lt;/div&gt;
&lt;div class="line"&gt;From beginning to end&lt;/div&gt;
&lt;div class="line"&gt;Christ the story, His the glory&lt;/div&gt;
&lt;div class="line"&gt;Alleluia! Amen!&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Honestly, I think it pretty much nailed it. It exactly captures the way the original song works, and the better you know the LOTR stories the more you appreciate some of the details. Even the scanning is better than I hoped - there are just a couple of places that would need fixing if you wanted to sing this.&lt;/p&gt;
&lt;p&gt;I particularly liked the verse about Samwise, which I wasn’t expecting — the other characters have more obvious parallels to draw on — and at points it’s really quite touching. ChatGPT presumably is drawing on a large and wonderful corpus of hymnody that helps shape its output, but it’s done a pretty good job of pulling all that together.&lt;/p&gt;
&lt;p&gt;Anyway, hope you enjoyed!&lt;/p&gt;
&lt;section id="updates"&gt;
&lt;h2&gt;Updates&lt;/h2&gt;
&lt;p&gt;Some fixes I would make:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;v2 Instead of “Left His throne to seek the hopeless / Now we rise at His redeeming” which doesn’t fit the story of Aragorn and also doesn’t scan, maybe something like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Passed the test and claimed his birthright&lt;/div&gt;
&lt;div class="line"&gt;Proved by hands that healing bring&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Tested, tried and proven worthy,&lt;/div&gt;
&lt;div class="line"&gt;Known by hands that healing bring&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;v4 Instead of “Loved beyond what we could send” which doesn’t exactly make sense, I would put:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Well deserves the name of Friend!&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;v1 My wife points out that Frodo doesn’t really conquer, he stumbles at the end, where Christ did not. I don’t know how to fix that.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</content>
    <category term="chatgpt" label="ChatGPT"/>
    <category term="christianity" label="Christianity"/>
  </entry>
  <entry>
    <title>Recursive project search in Emacs</title>
    <id>https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/</id>
    <updated>2024-12-30T19:34:33Z</updated>
    <published>2024-12-30T19:34:33Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/"/>
    <summary type="html">&lt;p&gt;The workflow of recursively searching for things or dealing with a list of issues to fix without getting lost.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Before reading this, you might want to check it out in &lt;a class="reference external" href="https://youtu.be/1jBbVUnNbDU"&gt;video form on Youtube&lt;/a&gt;, or with the embed below:&lt;/p&gt;
&lt;div class="center"&gt;
&lt;iframe align="center" width="560" height="315" src="https://www.youtube.com/embed/1jBbVUnNbDU?si=NRO0yLkWYRRPWxEn" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;

&lt;/div&gt;&lt;p&gt;Video is probably a more helpful format for demonstrating the workflow I’m talking about. Otherwise read on. If you’re coming from the video to look at the Elisp code, it is found towards the bottom of this post.&lt;/p&gt;
&lt;p&gt;“Recursive project search” is the name I’m giving to the flow where you do some kind of search to identify things that need to be done, but each of those tasks may leads you to do another search, etc. You need to complete all the sub-searches, but without losing your place in the parent searches.&lt;/p&gt;
&lt;p&gt;This is extremely common in software development and maintenance, whether you are just trying to scope out a set of changes, or whether actually doing them. In fact just about any task can end being some form of this, and you never know when it will turn out that way.&lt;/p&gt;
&lt;p&gt;This post is about how I use Emacs to do this, which is not rocket science but includes some tips and Elisp tweaks that can help a lot. When it comes to other editors or IDEs I’ve tried, I’ve never come close to finding a decent workflow for this, so I’ll have to leave it to other people to describe their approaches with other editors.&lt;/p&gt;
&lt;nav class="contents" id="contents" role="doc-toc"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#example-task-pyastgrep" id="toc-entry-1"&gt;Example task - pyastgrep&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#example-workflow" id="toc-entry-2"&gt;Example workflow&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#explanation" id="toc-entry-3"&gt;Explanation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#requirements" id="toc-entry-4"&gt;Requirements&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#unique-buffers-for-searches" id="toc-entry-5"&gt;Unique buffers for searches&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#other-tips" id="toc-entry-6"&gt;Other tips&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#external-tools" id="toc-entry-7"&gt;External tools&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#buffer-order-and-keeping-things-tidy" id="toc-entry-8"&gt;Buffer order and keeping things tidy&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#project-context" id="toc-entry-9"&gt;Project context&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#conclusion" id="toc-entry-10"&gt;Conclusion&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="example-task-pyastgrep"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-1" role="doc-backlink"&gt;Example task - pyastgrep&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’m going to take as an example my &lt;a class="reference external" href="https://github.com/spookylukey/pyastgrep/"&gt;pyastgrep&lt;/a&gt; project and a fairly simple refactoring I needed to do recently.&lt;/p&gt;
&lt;p&gt;For background, pyastgrep is a command line program and library that allows you to grep Python code at the level of Abstract Syntax Trees rather than just string. At the heart of this is a function that takes a Python path, and converts it to AST and also XML.&lt;/p&gt;
&lt;p&gt;The refactoring I want to do is make this function swappable, mostly so that users can apply different caching strategies to it. This is going to be a straightforward example of turning it into a parameter, or “dependency injection” if you want a fancy term. But that may involve modifying a number of functions in several layers of function calls.&lt;/p&gt;
&lt;p&gt;The function in question is &lt;a class="reference external" href="https://github.com/spookylukey/pyastgrep/blob/7935291013dd0aadb8cfa2ad6dc47f631bd48a5e/src/pyastgrep/files.py#L150"&gt;process_python_file&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="example-workflow"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-2" role="doc-backlink"&gt;Example workflow&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first step is a search, which in this case I will doing using &lt;a class="reference external" href="https://emacs-lsp.github.io/lsp-mode/"&gt;lsp-mode&lt;/a&gt;. I happen to use &lt;a class="reference external" href="https://github.com/emacs-lsp/lsp-pyright"&gt;lsp-pyright&lt;/a&gt; for Python, but there are other options.&lt;/p&gt;
&lt;p&gt;So I’ll kick off by opening the file, putting my cursor on the function &lt;code class="docutils literal"&gt;python_process_file&lt;/code&gt;, and calling &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; &lt;span class="pre"&gt;lsp-find-references&lt;/span&gt;&lt;/code&gt;. This returns a bunch of references. I can then step through them using &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; &lt;span class="pre"&gt;next-error&lt;/span&gt;&lt;/code&gt; and &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; &lt;span class="pre"&gt;previous-error&lt;/span&gt;&lt;/code&gt; — for which there are shortcuts defined, and I also do this so much that I have an &lt;code class="docutils literal"&gt;F&lt;/code&gt; key for it — &lt;code class="docutils literal"&gt;F2&lt;/code&gt; and &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;shift-F2&lt;/span&gt;&lt;/code&gt; respectively.&lt;/p&gt;
&lt;p&gt;Notice that in addition to the normal cursor, there is also a little triangle in the search results which shows the current result you are on.&lt;/p&gt;
&lt;figure class="align-center"&gt;
&lt;img alt="/blogmedia/emacs-recursive-search-lsp-find-references.png" class="full-bleed" src="https://lukeplant.me.uk/blogmedia/emacs-recursive-search-lsp-find-references.png"&gt;
&lt;/figure&gt;
&lt;p&gt;In this case, after the function definition itself, and an import, there is just one real usage – that last item in the search results.&lt;/p&gt;
&lt;p&gt;The details of doing this refactoring aren’t that important, but I’ll include some of the steps for completeness. The last result brings me to code like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-1" name="rest_code_c7bb4819f6de4458999236139e97dd00-1" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_python_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-2" name="rest_code_c7bb4819f6de4458999236139e97dd00-2" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-2"&gt;&lt;/a&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="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BinaryIO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-3" name="rest_code_c7bb4819f6de4458999236139e97dd00-3" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;query_func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;XMLQueryFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-4" name="rest_code_c7bb4819f6de4458999236139e97dd00-4" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;expression&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;a id="rest_code_c7bb4819f6de4458999236139e97dd00-5" name="rest_code_c7bb4819f6de4458999236139e97dd00-5" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Match&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ReadError&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;NonElementReturned&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-6" name="rest_code_c7bb4819f6de4458999236139e97dd00-6" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-7" name="rest_code_c7bb4819f6de4458999236139e97dd00-7" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-7"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-8" name="rest_code_c7bb4819f6de4458999236139e97dd00-8" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_c7bb4819f6de4458999236139e97dd00-9" name="rest_code_c7bb4819f6de4458999236139e97dd00-9" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c7bb4819f6de4458999236139e97dd00-9"&gt;&lt;/a&gt;    &lt;span class="n"&gt;processed_python&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process_python_file&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;/pre&gt;&lt;/div&gt;
&lt;p&gt;So I make &lt;code class="docutils literal"&gt;process_python_file&lt;/code&gt; a parameter:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-1" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-1" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_python_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-2" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-2" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-2"&gt;&lt;/a&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="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BinaryIO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-3" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-3" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;query_func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;XMLQueryFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-4" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-4" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;expression&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;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-5" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-5" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-5"&gt;&lt;/a&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-6" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-6" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;python_file_processor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&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;ProcessedPython&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ReadError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process_python_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-7" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-7" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-7"&gt;&lt;/a&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Match&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ReadError&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;NonElementReturned&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-8" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-8" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-9" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-9" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-9"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-10" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-10" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-10"&gt;&lt;/a&gt;
&lt;a id="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-11" name="rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-11" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_40d1c56b9eff4e6198f3d82b38ec75b2-11"&gt;&lt;/a&gt;    &lt;span class="n"&gt;processed_python&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;python_file_processor&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;/pre&gt;&lt;/div&gt;
&lt;p&gt;Having done this, I now need to search for all usages of the function I just modified, &lt;code class="docutils literal"&gt;search_python_file&lt;/code&gt;, so that I can pass the new parameter — another &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; &lt;span class="pre"&gt;lsp-find-references&lt;/span&gt;&lt;/code&gt;. I won’t go into the details this time, in this case it involves the following:&lt;/p&gt;
&lt;p&gt;Wherever &lt;code class="docutils literal"&gt;search_python_file&lt;/code&gt; is used, either:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;don’t pass &lt;code class="docutils literal"&gt;python_file_processor&lt;/code&gt;, because the default is what we want.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or do pass it, usually by similarly adding a &lt;code class="docutils literal"&gt;python_file_processor&lt;/code&gt; parameter to the calling function, and passing that parameter into &lt;code class="docutils literal"&gt;search_python_file&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This quickly gets me to &lt;code class="docutils literal"&gt;search_python_files&lt;/code&gt; (note the &lt;code class="docutils literal"&gt;s&lt;/code&gt;), and I find that it is imported in &lt;code class="docutils literal"&gt;pyastgrep.api&lt;/code&gt;. There are no usages to be fixed here, but it is exported in &lt;code class="docutils literal"&gt;__all__&lt;/code&gt;. This reminds me that the new parameter to this &lt;code class="docutils literal"&gt;search_python_files&lt;/code&gt; function is actually intended to be a part of the publicly documented API — in fact this is the whole reason I’m doing this change. This means I now need to fix the docs. Another search is needed, but this time a string-based grep in the docs folder. For this, I use ripgrep and &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; &lt;span class="pre"&gt;rg-project-all-files&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So now I have another buffer of results to get through – I’m about 4 levels deep at this point.&lt;/p&gt;
&lt;p&gt;Now comes one of the critical points in this workflow. I’ve completed the docs fix, and I’ve reached the end of that ripgrep buffer of search results:&lt;/p&gt;
&lt;figure class="align-center"&gt;
&lt;img alt="/blogmedia/emacs-recursive-search-ripgrep-end.png" class="full-bleed" src="https://lukeplant.me.uk/blogmedia/emacs-recursive-search-ripgrep-end.png"&gt;
&lt;/figure&gt;
&lt;p&gt;So I’ve come to a “leaf” of my search. But there were a whole load of other searches that I only got half way through. What happens now?&lt;/p&gt;
&lt;p&gt;All I do is kill these finished buffers – both the file I’m done with, and the the search buffer. And that puts me back to the previous search buffer, &lt;strong&gt;at exactly the point I left off&lt;/strong&gt;, with the cursor in the search buffer in the expected place.&lt;/p&gt;
&lt;figure class="align-center"&gt;
&lt;img alt="/blogmedia/emacs-recursive-search-lsp-find-references-search_python_files_continue.png" class="full-bleed" src="https://lukeplant.me.uk/blogmedia/emacs-recursive-search-lsp-find-references-search_python_files_continue.png"&gt;
&lt;/figure&gt;
&lt;p&gt;So I just continue. This process repeats itself, with any number of additional “side quests”, such as adding tests etc., until I get to the end of last search buffer, at which point I’m done.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="explanation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-3" role="doc-backlink"&gt;Explanation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;What I’ve basically done here is a &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Depth-first_search"&gt;depth-first recursive search&lt;/a&gt; over “everything that needs to be done to complete the task”. I started working on one thing, which lead to another and another, and for each one I went off and completed them as they came up.&lt;/p&gt;
&lt;p&gt;Doing a search like that requires keeping track of a fair amount of state, and if I were doing that in my head, I would get lost very quickly and forget what I was doing. So what I do instead is to use Emacs buffers to maintain all of that state.&lt;/p&gt;
&lt;p&gt;The buffers themselves form a stack, and each buffer has a cursor within it which tells me how far through the list of results I am. (This is equivalent to how recursive function calls typically work in a program - there will be a stack of function calls, each with local variables stored in a frame somehow).&lt;/p&gt;
&lt;p&gt;In the above case I only went about 4 or 5 levels deep, and each level was fairly short. But you can go much deeper and not get lost, because the buffers are maintaining all the state you need. You can be 12 levels down, deep in the woods, and then just put it to one side and come back after lunch, or the next day, and just carry on, because the buffers are remembering everything for you, and the buffer that is in front of you tells you what you were doing last.&lt;/p&gt;
&lt;p&gt;It doesn’t even matter if you switch between buffers and get them a bit out of order – you just have to ensure that you get to the bottom of each one.&lt;/p&gt;
&lt;p&gt;Another important feature is that you can use &lt;strong&gt;different kinds of search&lt;/strong&gt;, and
mix and match them as you need, such as &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;lsp-find-references&lt;/span&gt;&lt;/code&gt; and the ripgrep
searches above. In addition to these two, you can insert other
“search-like” things, like linters, static type checkers, compilers and build
processes – anything that will return a list of items to check. So this is not a
feature of just one mode, it’s a feature of how buffers work together.&lt;/p&gt;
&lt;p&gt;At each step, you can also apply different kinds of fixes – e.g. instead of manually editing, you might be using &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; &lt;span class="pre"&gt;lsp-rename&lt;/span&gt;&lt;/code&gt; on each instance, and you might be using keyboard macros etc.&lt;/p&gt;
&lt;p&gt;When I’ve attempted to use editors other than Emacs, this is one of the things I’ve missed most of all. Just getting them to do the most basic requirement of “do a search, without overwriting the previous search results” has been a headache or impossible – although I may have given up too soon.&lt;/p&gt;
&lt;p&gt;I’m guessing people manage somehow – or perhaps not: I’ve sometimes noticed that I’ve been happy to take on tasks that involved this kind of workflow which appeared to be daunting to other people, and completed them without problem, which was apparently impressive to others. I also wonder whether difficulties in using search in an editor drive a reluctance to take on basic refactoring tasks, such as manual renames, or an inability to complete them correctly. If so, it would help to explain why codebases often have many basic problems (like bad names).&lt;/p&gt;
&lt;p&gt;In any case, I don’t think I can take for granted that people can do this, so that’s why I’m bothering to post about it!&lt;/p&gt;
&lt;/section&gt;
&lt;section id="requirements"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-4" role="doc-backlink"&gt;Requirements&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;What do you need in Emacs for this to work? Or what equivalent functionality is needed if you want to reproduce this elsewhere?&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;First, the search buffer must have the ability to remember your position. All the search modes I’ve seen in Emacs do this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, you need an easy way to step through results, and this is provided by the really convenient &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; &lt;span class="pre"&gt;next-error&lt;/span&gt;&lt;/code&gt; function which does exactly what you want (see the Emacs docs for it, which describe how it works). This function is part of Compilation Mode or Compilation Minor Mode, and all the search modes I’ve seen use this correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Thirdly, the search modes mustn’t re-use buffers for different searches, otherwise you’ll clobber earlier search results that you hadn’t finished processing. Some modes do this automatically, others have a habit of re-using buffers — but we can fix that:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;section id="unique-buffers-for-searches"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-5" role="doc-backlink"&gt;Unique buffers for searches&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If search commands re-use search buffers by default, I’ve found it’s usually pretty easy to override the behaviour so that you automatically always get a unique buffer for each new search you do.&lt;/p&gt;
&lt;p&gt;The approach you need often varies slightly for each mode, but the basic principle is similar - different searches should get different &lt;strong&gt;buffer names&lt;/strong&gt;, and you can use some &lt;a class="reference external" href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Advising-Functions.html"&gt;Elisp Advice&lt;/a&gt; to insert the behaviour you want.&lt;/p&gt;
&lt;p&gt;So here are the main ones that I override:&lt;/p&gt;
&lt;p&gt;rg.el for ripgrep searching:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code elisp"&gt;&lt;a id="rest_code_35b67af16051499dbf5ae6ea873905b9-1" name="rest_code_35b67af16051499dbf5ae6ea873905b9-1" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_35b67af16051499dbf5ae6ea873905b9-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defadvice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;rg-run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;before&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;rg-run-before&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_35b67af16051499dbf5ae6ea873905b9-2" name="rest_code_35b67af16051499dbf5ae6ea873905b9-2" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_35b67af16051499dbf5ae6ea873905b9-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;rg-save-search&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is just using the &lt;a class="reference external" href="https://rgel.readthedocs.io/en/latest/usage.html#search-management"&gt;save feature built in to rg.el&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This goes in your &lt;code class="docutils literal"&gt;init.el&lt;/code&gt;. Since I use &lt;a class="reference external" href="https://jwiegley.github.io/use-package/"&gt;use-package&lt;/a&gt; I usually put it inside the relevant &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;(use-package&lt;/span&gt; :config)&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;For &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;lsp-find-references&lt;/span&gt;&lt;/code&gt; I use the following which makes a unique buffer name based on the symbol being searched for:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code elisp"&gt;&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-1" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-1" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;advice-add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'lsp-find-references&lt;/span&gt;
&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-2" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-2" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;:around&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;my/lsp-find-references-unique-buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-3" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-3" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-3"&gt;&lt;/a&gt;
&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-4" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-4" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-4"&gt;&lt;/a&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my/lsp-find-references-unique-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;orig-func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;rest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-5" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-5" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-5"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;"Gives lsp-find-references a unique buffer name, to help with recursive search."&lt;/span&gt;
&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-6" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-6" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-6"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt;
&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-7" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-7" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-7"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;xref-buffer-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"%s %s"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;xref-buffer-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;symbol-at-point&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;a id="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-8" name="rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-8" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_c015fd3fbb1f422ead9e4da7aa335ccd-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;orig-func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then for general &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; compile&lt;/code&gt; or &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;projectile-compile-project&lt;/span&gt;&lt;/code&gt; commands I use the following, which gives a unique buffer name based on the compilation command used:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code elisp"&gt;&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-1" name="rest_code_249d03f491f34152bb8ec317528ee5e9-1" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;advice-add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'compilation-start&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-2" name="rest_code_249d03f491f34152bb8ec317528ee5e9-2" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;:around&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;my/compilation-unique-buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-3" name="rest_code_249d03f491f34152bb8ec317528ee5e9-3" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-3"&gt;&lt;/a&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-4" name="rest_code_249d03f491f34152bb8ec317528ee5e9-4" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-4"&gt;&lt;/a&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my/compilation-unique-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;orig-func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;rest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-5" name="rest_code_249d03f491f34152bb8ec317528ee5e9-5" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-5"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;"Give compilation buffers a unique name so that new compilations get new&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-6" name="rest_code_249d03f491f34152bb8ec317528ee5e9-6" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-6"&gt;&lt;/a&gt;&lt;span class="s"&gt;buffers. This helps with recursive search.&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-7" name="rest_code_249d03f491f34152bb8ec317528ee5e9-7" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-8" name="rest_code_249d03f491f34152bb8ec317528ee5e9-8" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-8"&gt;&lt;/a&gt;&lt;span class="s"&gt;If a compile command is run starting from the compilation buffer,&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-9" name="rest_code_249d03f491f34152bb8ec317528ee5e9-9" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-9"&gt;&lt;/a&gt;&lt;span class="s"&gt;the buffer will be re-used, but if it is started from a different&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-10" name="rest_code_249d03f491f34152bb8ec317528ee5e9-10" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-10"&gt;&lt;/a&gt;&lt;span class="s"&gt;buffer a new compilation buffer will be created."&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-11" name="rest_code_249d03f491f34152bb8ec317528ee5e9-11" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-11"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;car&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-12" name="rest_code_249d03f491f34152bb8ec317528ee5e9-12" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-12"&gt;&lt;/a&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;compilation-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;orig-func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-13" name="rest_code_249d03f491f34152bb8ec317528ee5e9-13" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-13"&gt;&lt;/a&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;new-buffer-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buffer-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;compilation-buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-14" name="rest_code_249d03f491f34152bb8ec317528ee5e9-14" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-14"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-current-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;compilation-buffer&lt;/span&gt;
&lt;a id="rest_code_249d03f491f34152bb8ec317528ee5e9-15" name="rest_code_249d03f491f34152bb8ec317528ee5e9-15" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_249d03f491f34152bb8ec317528ee5e9-15"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rename-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;new-buffer-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I use this mode quite a lot via &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; compile&lt;/code&gt; or &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt;
&lt;span class="pre"&gt;projectile-compile-project&lt;/span&gt;&lt;/code&gt; to do things other than compilation – for custom
search commands, like &lt;a class="reference external" href="https://pyastgrep.readthedocs.io/en/latest/"&gt;pyastgrep&lt;/a&gt;, for linters and static
checkers.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="other-tips"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-6" role="doc-backlink"&gt;Other tips&lt;/a&gt;&lt;/h2&gt;
&lt;section id="external-tools"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-7" role="doc-backlink"&gt;External tools&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Many of externals tools that you might run via &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; compile&lt;/code&gt; already have output that is in exactly the format that Emacs &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;compilation-mode&lt;/span&gt;&lt;/code&gt; expects, so that search results or error messages become hyperlinked as expected inside Emacs. For example, &lt;a class="reference external" href="https://mypy.readthedocs.io/en/stable/"&gt;mypy&lt;/a&gt; has the right format by default, and most older compilers like gcc etc.&lt;/p&gt;
&lt;p&gt;For those tools that don’t, sometimes you can tweak the options of how they print. For example, if you run &lt;a class="reference external" href="https://github.com/BurntSushi/ripgrep"&gt;ripgrep&lt;/a&gt; as &lt;code class="docutils literal"&gt;rg &lt;span class="pre"&gt;--no-heading&lt;/span&gt;&lt;/code&gt; (just using normal &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;M-x&lt;/span&gt; compile&lt;/code&gt;, without a dedicated ripgrep mode), it produces the necessary format.&lt;/p&gt;
&lt;p&gt;Alternatively, you can make custom wrappers that fix the format. For example, I’ve got this one-liner bash script to wrap &lt;a class="reference external" href="https://github.com/microsoft/pyright"&gt;pyright&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code bash"&gt;&lt;a id="rest_code_076c38269aa84bc4bff721e5b14df257-1" name="rest_code_076c38269aa84bc4bff721e5b14df257-1" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_076c38269aa84bc4bff721e5b14df257-1"&gt;&lt;/a&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;a id="rest_code_076c38269aa84bc4bff721e5b14df257-2" name="rest_code_076c38269aa84bc4bff721e5b14df257-2" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_076c38269aa84bc4bff721e5b14df257-2"&gt;&lt;/a&gt;&lt;span class="c1"&gt;# Adjust ANSI colour and codes and whitespace output from pyright to make it work nicer in Emacs&lt;/span&gt;
&lt;a id="rest_code_076c38269aa84bc4bff721e5b14df257-3" name="rest_code_076c38269aa84bc4bff721e5b14df257-3" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#rest_code_076c38269aa84bc4bff721e5b14df257-3"&gt;&lt;/a&gt;pyright&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'s/\x1b\[[0-9;]*[mGK]//g'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{$1=$1};1'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'s/\(.*:[0-9]*:[0-9]*\) -/\1: -/g'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;section id="buffer-order-and-keeping-things-tidy"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-8" role="doc-backlink"&gt;Buffer order and keeping things tidy&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Emacs typically manages your buffers as a stack. However, it’s very easy for things to get out of order as you are jumping around files or search buffers. It doesn’t matter too much if you go through the search buffers in the “wrong” order – as long as you get to the bottom of all of them. But to be sure I have got to the bottom, I do two things:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Before starting anything that I anticipate will be more than a few levels deep, I tidy up by closing all buffers but the one I’m working on. You can do this with &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;crux-kill-other-buffers&lt;/span&gt;&lt;/code&gt; from &lt;a class="reference external" href="https://github.com/bbatsov/crux"&gt;crux&lt;/a&gt;. I have my own version that is more customised for my needs — &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;crux-kill-other-buffers&lt;/span&gt;&lt;/code&gt; only kills buffers that are files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;At the end, when I think I’m done, I check the buffer list (&lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;C-x&lt;/span&gt; &lt;span class="pre"&gt;C-b&lt;/span&gt;&lt;/code&gt;) for anything else I missed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="project-context"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-9" role="doc-backlink"&gt;Project context&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In order to limit the scope of searches to my project, I’m typically leaning on &lt;a class="reference external" href="https://github.com/bbatsov/projectile"&gt;projectile.el&lt;/a&gt;, but there are &lt;a class="reference external" href="https://docs.projectile.mx/projectile/projectile_vs_project.html"&gt;other options&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/recursive-project-search-in-emacs/#toc-entry-10" role="doc-backlink"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I hope this post has been helpful, and if you’ve got additional tips for this kind of workflow, please leave a comment!&lt;/p&gt;
&lt;/section&gt;</content>
    <category term="emacs" label="Emacs"/>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
  </entry>
</feed>
