<?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 (Posts about Software development)</title>
  <id>https://lukeplant.me.uk/blog/categories/software-development.xml</id>
  <updated>2026-05-02T11:46:16Z</updated>
  <author>
    <name>Luke Plant</name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://lukeplant.me.uk/blog/categories/software-development.xml"/>
  <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/categories/software-development/"/>
  <generator uri="https://getnikola.com/">Nikola</generator>
  <entry>
    <title>Inverse Sapir-Whorf and programming languages</title>
    <id>https://lukeplant.me.uk/blog/posts/inverse-sapir-whorf-and-programming-languages/</id>
    <updated>2026-05-01T09:40:36+01:00</updated>
    <published>2026-05-01T09:40:36+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/inverse-sapir-whorf-and-programming-languages/"/>
    <summary type="html">&lt;p&gt;A discussion on how features of programming languages can make it hard to avoid expressing or talking about things you may or may not care about as a programmer.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;The &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Linguistic_relativity"&gt;Sapir-Whorf hypothesis&lt;/a&gt;, in its simplest form, is the idea that the language you speak influences the thoughts you think. This post is about a twist on this idea, that I’m calling “Inverse Sapir-Whorf” (for want of a better term), and how we see it in computer programming languages.&lt;/p&gt;
&lt;p&gt;Sapir-Whorf is one of those ideas that has been popularised in general culture in a rather misrepresented and &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Arrival_(film)"&gt;exaggerated form&lt;/a&gt;. In the field of linguistics, not many people today take seriously the “strong” forms of Sapir-Whorf, such as “linguistic determinism” – the idea that a language controls your thoughts or limits what you can think, or that you even need certain languages to think certain thoughts.&lt;/p&gt;
&lt;p&gt;For example, just because a language might lack grammatical tenses, it doesn’t at all follow that the speakers will be more limited in how they think about time – there are always other ways you can express time.&lt;/p&gt;
&lt;p&gt;There is a fair amount of evidence that spoken languages can affect perception, skill and attitudes in certain areas, but it’s usually hard to demonstrate a large direct effect.&lt;/p&gt;
&lt;p&gt;Inverse Sapir-Whorf is a bit different. I haven’t been able to track down where I first came across the idea, but it goes like this: if classic Sapir-Whorf says your language limits what you can say or think, or makes it hard to say some things, inverse Sapir-Whorf says your language limits what you &lt;strong&gt;can’t&lt;/strong&gt; say, or makes it hard &lt;strong&gt;not&lt;/strong&gt; to say some things, or even hard not to think about some things. Some examples might clear things up.&lt;/p&gt;
&lt;section id="examples-in-natural-language"&gt;
&lt;h2&gt;Examples in natural language&lt;/h2&gt;
&lt;p&gt;There are many examples to choose from, but they are not always obvious to native speakers of a language. I’ll pick just a few.&lt;/p&gt;
&lt;section id="english-temporary-or-permanent-present-tense"&gt;
&lt;h3&gt;English: temporary or permanent present tense&lt;/h3&gt;
&lt;p&gt;What’s the difference between someone saying “I’m living in London” and “I live in London”? A non-native speaker may not pick this up at all, and a native speaker may pick it up only subconsciously, but “I’m living in London” reveals that the arrangement is temporary.&lt;/p&gt;
&lt;p&gt;Now, this might not even be to do with the actual length of time you have been living there, because “temporary” is pretty relative. It might be more about how much you &lt;em&gt;like&lt;/em&gt; London. You have to choose a tense, and because you typically do so subconsciously, the language is forcing you to reveal things – either the period of time you’ve been living somewhere, or how you feel about it.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="english-turkish-french-gendered-pronouns-and-nouns"&gt;
&lt;h3&gt;English/Turkish/French: gendered pronouns and nouns&lt;/h3&gt;
&lt;p&gt;In English, in normal speech you are going to use “he” or “she” when referring to a specific person. “Singular they” does exist, but it’s very unnatural if you are talking about a specific person of known or assumed sex.&lt;/p&gt;
&lt;p&gt;You can compare this to another language which doesn’t have gendered pronouns, such as Turkish, which just has “o” for he/she/it. The lack of gendered pronouns in Turkish doesn’t stop you from thinking or talking about a person’s sex, or produce a “less gendered society”, or anywhere close, so it would be difficult to find support for normal Sapir-Whorf here. But the inverse Sapir-Whorf is obvious – English pronouns push you to talk about it whether you want to or not. If you are trying to talk about someone you know, but do so anonymously, it can be very hard to avoid making their identification easier by revealing their sex with an inadvertent “him” or “her”.&lt;/p&gt;
&lt;p&gt;Different again is French, in which &lt;em&gt;nouns&lt;/em&gt; are gendered, which in some cases can force you to reveal information. If you translate “my friend” into French, you have to choose between “mon ami” (male friend) and “mon amie” (female friend), which are distinct, at least in written form, or “mon copain” vs “ma copine”. Possessive pronouns are also interesting – they are gendered in both English and French (his/her, son/sa), but refer to the gender of the possessor and possessee respectively, and so reveal different information.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="turkish-mis-tense"&gt;
&lt;h3&gt;Turkish: “mış” tense&lt;/h3&gt;
&lt;p&gt;With some simplifications, Turkish has two main past tenses: there is the normal one that is similar to “simple past” in English, and then there is the “mış” form (you can pronounce that “mish” if you want).&lt;/p&gt;
&lt;p&gt;This has &lt;a class="reference external" href="https://www.turkishtextbook.com/beginner-mis-forms/"&gt;various functions&lt;/a&gt;, but when describing a past event, this form is used when you have second hand or unreliable information. If someone asks you “Did Fred come to work on Monday?”, then if you saw him you would use the normal past tense “geldi” (he came), but if you only &lt;em&gt;heard&lt;/em&gt; that he came you would instead say “gelmiş” (he came, but second hand information).&lt;/p&gt;
&lt;p&gt;The interesting thing to me as a non-native speaker was the effect of having these options, in contrast to English where you can just use simple past tense without any specific indication of reliability or where the information came from. In certain circumstances, Turkish forces you to include information about your level of certainty or whether you witnessed something –  the simple past form is not neutral, because the existence of the “mış” form makes it an unnatural choice if it is not the most appropriate of the two.&lt;/p&gt;
&lt;p&gt;Interestingly, having learned to think that way, my wife and I have noticed an effect on our English. Often in Turkish the “mış” suffix would come at the end of the last word in a sentence, so now quite frequently we get to the end of an English sentence and notice that we haven’t put in any marker for “this-is-second-hand-info-I-didn’t-actually-witness-it”, and so we tack “mış” on the end.&lt;/p&gt;
&lt;p&gt;Of course, you can easily express the same thing in English, using words like “apparently” and other means, but English doesn’t &lt;strong&gt;force&lt;/strong&gt; you to specify, while Turkish pretty much does.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="comments"&gt;
&lt;h3&gt;Comments&lt;/h3&gt;
&lt;p&gt;You often don’t notice these things until you learn another language, or attempt to teach your language to a foreigner. You kind of just understand them subconsciously. The vast majority of times you choose simple present over present continuous, for example, you won’t be &lt;strong&gt;consciously&lt;/strong&gt; thinking about what that implies.&lt;/p&gt;
&lt;p&gt;I should also note that when a language forces you express something, it might not be in the form of something &lt;em&gt;included&lt;/em&gt;, but in something &lt;em&gt;omitted&lt;/em&gt;. For example, I might say “I love cake” or “I love the cake”. In the first case, I’m talking about cake generally, in the second about a specific cake. It is the absence of the word “the” in the first case that makes it unambiguous that I’m referring to all cake, because if I’m referring to a specific cake, I &lt;strong&gt;must&lt;/strong&gt; use the word “the” or some other marker like “this”. In another language, there might not be a direct equivalent to this distinction.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="examples-in-programming"&gt;
&lt;h2&gt;Examples in programming&lt;/h2&gt;
&lt;p&gt;When it comes to programming languages, I think that the “straight” version of  Sapir-Whorf is closer to being true - in some programming languages it is simply hard to express certain concepts. For example, in a language like Python or Haskell it’s hard (though not impossible) to talk about memory allocations. We often talk about the limitations of a language in terms of “things that are hard to express” in that language. Hillel Wayne has some more discussion of this in his post &lt;a class="reference external" href="https://buttondown.com/hillelwayne/archive/sapir-whorf-does-not-apply-to-programming/"&gt;Sapir-Whorf does not apply to Programming Languages&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But I want to talk more about Inverse Sapir-Whorf. What is the language forcing you to talk about, even if you don’t actually care about it?&lt;/p&gt;
&lt;p&gt;I think there are actually many, many examples of this, but seeing them can be quite hard, and often requires the “foreigner perspective” that comes from learning multiple languages.&lt;/p&gt;
&lt;p&gt;Here are a few:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Most languages force you to express the order in which computation should be done. For example, in Python:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_11fb4ed97b504dcf83b998b745f49865-1" name="rest_code_11fb4ed97b504dcf83b998b745f49865-1" href="https://lukeplant.me.uk/blog/posts/inverse-sapir-whorf-and-programming-languages/#rest_code_11fb4ed97b504dcf83b998b745f49865-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;some_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&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="n"&gt;z&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;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here you are saying:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;first compute &lt;code class="docutils literal"&gt;y + 1&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then compute &lt;code class="docutils literal"&gt;z + 2&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then pass these two values as arguments to &lt;code class="docutils literal"&gt;some_func&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might not be very conscious of specifying this ordering, but you are doing it, and in Python, there isn’t a way to express the above computation which doesn’t also specify order. Most languages are similar, although in some &lt;a class="reference external" href="https://en.cppreference.com/w/c/language/eval_order.html"&gt;it gets very complicated&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A few languages are very different, however. In Haskell, in an equivalent expression like &lt;code class="docutils literal"&gt;some_func (y + 1) (z + 2)&lt;/code&gt;, due to &lt;a class="reference external" href="https://wiki.haskell.org/Non-strict_semantics"&gt;non-strict semantics&lt;/a&gt; you are not specifying an order of evaluation at all. This enables some clever tricks, like referring to values you haven’t defined yet (see &lt;a class="reference external" href="https://wiki.haskell.org/Tying_the_Knot"&gt;Tying the knot&lt;/a&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/"&gt;Function colouring for async&lt;/a&gt; is another good example. In languages like Javascript or Python with an explicit &lt;code class="docutils literal"&gt;async&lt;/code&gt; keyword, you have to talk about whether code is sync or async.&lt;/p&gt;
&lt;p&gt;In the case of “sync” functions, you do it by omission of the &lt;code class="docutils literal"&gt;async&lt;/code&gt; keyword, but you are still choosing between the two options, and there is no way to write code that is ambivalent on the subject.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Most languages without &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)"&gt;garbage collection&lt;/a&gt; force you to talk about memory allocation and de-allocation.&lt;/p&gt;
&lt;p&gt;For languages like C, you normally do this fairly explicitly – or implicitly use stack allocation, but you’ve still got to make that choice.  In other languages, it can become more hidden, but doesn’t really go away. In Rust, for example, in gets converted into talk about lifetimes or explicit reference counting. Saying “I just don’t care about when the memory for this gets allocated or de-allocated, please deal with it” is not really one of your options.&lt;/p&gt;
&lt;p&gt;Of course, not talking about memory allocation also has a cost. In that case, the language will almost certainly need to put a lot of things on &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Memory_management#HEAP"&gt;the heap&lt;/a&gt; and have a runtime garbage collector. However it may also have significant freedom to choose for you – Haskell will often be able to do this using &lt;a class="reference external" href="https://wiki.haskell.org/Performance/Strictness"&gt;strictness analysis&lt;/a&gt;, for example.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All modern languages I’m aware of force you to think about “scope”. In many cases, scope is expressed by the physical place in which you put a variable, with some additional syntax if you want something different (like &lt;a class="reference external" href="https://docs.python.org/3/reference/simple_stmts.html#the-global-statement"&gt;global&lt;/a&gt; or &lt;a class="reference external" href="https://docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement"&gt;nonlocal&lt;/a&gt; in Python). If you never want to think about scope, you probably have to drop to assembly and live with a single global address space.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Statically typed languages often force you to think about and talk about the type of every variable. This is lessened somewhat by type inference, as the “conversation” involves a more “intelligent” listener who can pick up more things from context, but it’s still there.&lt;/p&gt;
&lt;p&gt;Pure dynamically typed languages still allow you to talk about types – for example, using things like &lt;code class="docutils literal"&gt;isinstance&lt;/code&gt; checks in Python, but it is more unnatural (and technically it’s a different thing anyway).&lt;/p&gt;
&lt;p&gt;In contrast to both of them, one of the attractions of &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Gradual_typing"&gt;gradually typed languages&lt;/a&gt; is that they genuinely avoid the inverse Sapir-Whorf problem, and allow you the freedom to talk about types, or not, at your preference. I’m not sure how well this works in practice - the existing code base conventions and the linters in use always put pressure in some direction.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I suspect that many of the features of more “approachable” or “readable” programming languages could be analysed in these terms – they have a low inverse Sapir-Whorf barrier, and don’t force you to talk about things you don’t have an opinion on, and may not even understand yet.&lt;/p&gt;
&lt;p&gt;Are there more examples of this that you’ve come across? How do they affect the programming languages we use, or how we perceive them?&lt;/p&gt;
&lt;/section&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/hb9tdr/inverse_sapir_whorf_programming"&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="haskell" label="Haskell"/>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
  </entry>
  <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>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>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>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>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>
  <entry>
    <title>Keeping things in sync: derive vs test</title>
    <id>https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/</id>
    <updated>2024-06-28T10:15:00+01:00</updated>
    <published>2024-06-28T10:15:00+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/"/>
    <summary type="html">&lt;p&gt;There are times when we need to stop trying to make everything sync automatically, and just test that it is synced. Tips for Python and web dev.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;An extremely common problem in programming is that multiple parts of a program need to be kept in sync – they need to do exactly the same thing or behave in a consistent way. It is in response to this problem that we have mantras like “DRY” (Don’t Repeat Yourself), or, as I prefer it, &lt;a class="reference external" href="https://wiki.c2.com/?OnceAndOnlyOnce"&gt;OAOO&lt;/a&gt;, “Each and every declaration of behaviour should appear Once And Only Once”.&lt;/p&gt;
&lt;p&gt;For both of these mantras, if you are faced with possible duplication of any kind, the answer is simply “just say no”. However, since programming mantras are to be understood as &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/programming-mantras-are-proverbs/"&gt;proverbs&lt;/a&gt;, not absolute laws, there are times that obeying this mantra can hurt more than it helps, so in this post I’m going to discuss other approaches.&lt;/p&gt;
&lt;p&gt;Most of what I say is fairly language agnostic I think, but I’ve got specific tips for Python and web development.&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/keeping-things-in-sync-derive-vs-test/#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/keeping-things-in-sync-derive-vs-test/#the-essential-problem" id="toc-entry-1"&gt;The essential 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/keeping-things-in-sync-derive-vs-test/#the-ideal-solution-derive" id="toc-entry-2"&gt;The ideal solution: derive&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/keeping-things-in-sync-derive-vs-test/#alternative-solution-test" id="toc-entry-3"&gt;Alternative solution: test&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/keeping-things-in-sync-derive-vs-test/#examples" id="toc-entry-4"&gt;Examples&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/keeping-things-in-sync-derive-vs-test/#example-1-external-data-sources" id="toc-entry-5"&gt;Example 1 - external data sources&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/keeping-things-in-sync-derive-vs-test/#example-2-defining-ui-behaviour-for-domain-objects" id="toc-entry-6"&gt;Example 2 - defining UI behaviour for domain objects&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/keeping-things-in-sync-derive-vs-test/#example-3-external-polymorphism-and-static-typing" id="toc-entry-7"&gt;Example 3 - external polymorphism and static typing&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/keeping-things-in-sync-derive-vs-test/#example-4-generated-code" id="toc-entry-8"&gt;Example 4 - generated code&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/keeping-things-in-sync-derive-vs-test/#conclusion" id="toc-entry-9"&gt;Conclusion&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/keeping-things-in-sync-derive-vs-test/#links" id="toc-entry-10"&gt;Links&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="the-essential-problem"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-1" role="doc-backlink"&gt;The essential problem&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To step back for a second, the essential problem that we are addressing here is that if making a change to a certain behaviour requires changing more than one place in the code, we have the risk that one will be forgotten. This results in bugs, which can be of various degrees of seriousness depending on the code in question.&lt;/p&gt;
&lt;p&gt;To pick a concrete example, suppose we have a rule that says that items in a deleted folder get stored for 30 days, then expunged. We’re going to need some code that does the actual expunging after 30 days, but we’re also going to need to tell the user about the limit somewhere in the user interface. “Once And Only Once” says that the 30 days limit needs to be defined in a single place somewhere, and then reused.&lt;/p&gt;
&lt;p&gt;There is a second kind of motivating example, which I think often crops up  when people quote “Don’t Repeat Yourself”, and it’s really about avoiding tedious things from a developer perspective. Suppose you need to add an item to a menu, and you find out that first you’ve got to edit the &lt;code class="docutils literal"&gt;MENU_ITEMS&lt;/code&gt; file to add an entry, then you’ve got to edit the &lt;code class="docutils literal"&gt;MAIN_MENU&lt;/code&gt; constant to refer to the new entry, then you’ve got to define a keyboard shortcut in the &lt;code class="docutils literal"&gt;MENU_SHORTCUTS&lt;/code&gt; file, then a menu icon somewhere else etc. All of these different places are in some way repeating things about how menus work. I think this is less important in general, but it is certainly life-draining as a developer if code is structured in this way, especially if it is difficult to discover or remember all the things that have to be done.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="the-ideal-solution-derive"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-2" role="doc-backlink"&gt;The ideal solution: derive&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;OAOO and DRY say that we aim to have a single place that defines the rule or logic, and any other place should be &lt;strong&gt;derived&lt;/strong&gt; from this.&lt;/p&gt;
&lt;p&gt;Regarding the simple example of a time limit displayed in the UI and used in the backend, this might be as simple as defining a constant e.g. in Python:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_84e9834c384141c08219938e070d9aa8-1" name="rest_code_84e9834c384141c08219938e070d9aa8-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_84e9834c384141c08219938e070d9aa8-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;a id="rest_code_84e9834c384141c08219938e070d9aa8-2" name="rest_code_84e9834c384141c08219938e070d9aa8-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_84e9834c384141c08219938e070d9aa8-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_84e9834c384141c08219938e070d9aa8-3" name="rest_code_84e9834c384141c08219938e070d9aa8-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_84e9834c384141c08219938e070d9aa8-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;EXPUNGE_TIME_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We then &lt;code class="docutils literal"&gt;import&lt;/code&gt; and use this constant in both our UI and backend.&lt;/p&gt;
&lt;p&gt;An important part of this approach is that the “deriving” process should be entirely automatic, not something that you can forget to do. In the case of a Python &lt;code class="docutils literal"&gt;import&lt;/code&gt; statement, that is very easy to achieve, and relatively hard to get wrong – if you change the constant where it is defined in one module, any other code that uses it will pick up the change the next time the Python process is restarted.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="alternative-solution-test"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-3" role="doc-backlink"&gt;Alternative solution: test&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;By “test”, I mean ideally an automated test, but manual tests may also work if they are properly scripted. The idea is that you write a test that checks the behaviour of code is synced. Often, it may be that for one (or more) instances that need the behaviour will define it using some constant as above, let’s say the “backend” code. Then, for one instance, e.g. the UI, you would hard code “30 days” without using the constant, but have a test that uses the backend constant to build a string, and checks the UI for that string.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="examples"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-4" role="doc-backlink"&gt;Examples&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the example above, it might be hard to see why you want to use the fundamentally less reliable, less automatic method I’m suggesting. So I now have to show some motivating examples where the “derive” method ends up losing to the cruder, simpler alternative of “test”.&lt;/p&gt;
&lt;section id="example-1-external-data-sources"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-5" role="doc-backlink"&gt;Example 1 - external data sources&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;My first example comes from the project I’m currently working on, which involves
creating &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Computer-aided_manufacturing"&gt;CAM&lt;/a&gt;
files from input data. Most of the logic for that is driven using code, but
there are some dimensions that are specified as data tables by the engineers of
the physical product.&lt;/p&gt;
&lt;p&gt;These data tables look something like below. The details here aren’t important, and I’ve changed them – it’s enough to know that we’ve are creating some physical “widgets” which need to have specific dimensions specified:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th class="head" colspan="3"&gt;&lt;p&gt;Widgets have length 150mm unless specified below&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;th class="head"&gt;&lt;p&gt;Widget id&lt;/p&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;p&gt;Location&lt;/p&gt;&lt;/th&gt;
&lt;th class="head"&gt;&lt;p&gt;Length (mm)&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;A&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;start&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;100&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;A&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;end&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;120&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;F&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;start&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;105&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;p&gt;F&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;end&lt;/p&gt;&lt;/td&gt;
&lt;td&gt;&lt;p&gt;110&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These tables are supplied at design-time rather than run-time i.e. they are bundled with the software and can’t be changed after the code is shipped. But it is still convenient to read them in automatically rather than simply duplicate the tables in my code by some process. So, for the body of the table, that’s exactly what my code does on startup – it reads the bundled XLSX/CSV files.&lt;/p&gt;
&lt;p&gt;So we are obeying “derive” here — there is a single, canonical source of data, and anywhere that needs it derives it by an entirely automatic process.&lt;/p&gt;
&lt;p&gt;But what about that “150mm” default value specified in the header of that table?&lt;/p&gt;
&lt;p&gt;It would be possible to “derive” it by having a parser. Writing such a parser is not hard to do – for this kind of thing in Python I like &lt;a class="reference external" href="https://github.com/python-parsy/parsy/"&gt;parsy&lt;/a&gt;, and it is as simple as:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-1" name="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;parsy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;P&lt;/span&gt;
&lt;a id="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-2" name="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-3" name="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;default_length_parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-4" name="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-4"&gt;&lt;/a&gt;  &lt;span class="n"&gt;P&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="s2"&gt;"Widgets have length "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-5" name="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-5"&gt;&lt;/a&gt;  &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"\d+"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&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;a id="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-6" name="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-6"&gt;&lt;/a&gt;  &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;P&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="s2"&gt;"mm unless specified below"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-7" name="rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2bdec6da0c94ecb9a9ae2c5a48f60eb-7"&gt;&lt;/a&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In fact I do something similar in some cases. But in reality, the “parser” here is pretty simplistic – it can’t deal with the real variety of English text that might be put into the sentence, and to claim I’m “deriving” it from the table is a bit of a stretch – I’m just matching a specific, known pattern. In addition, it’s probably not the case that &lt;strong&gt;any&lt;/strong&gt; value for the default length would work – most likely if it was 10 times larger, there would be some other problem, and I’d want to do some manual checking.&lt;/p&gt;
&lt;p&gt;So, let’s admit that we are really just checking for something expected, using the “test” approach. You can still define a constant that you use in most of the code:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ce0c9d63dc1e421f88efe3c93ce7a75d-1" name="rest_code_ce0c9d63dc1e421f88efe3c93ce7a75d-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ce0c9d63dc1e421f88efe3c93ce7a75d-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;DEFAULT_LENGTH_MM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And then you test it is what you expect when you load the data file:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_8e5941c2ac244aa08829366bd12f135b-1" name="rest_code_8e5941c2ac244aa08829366bd12f135b-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_8e5941c2ac244aa08829366bd12f135b-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;worksheets&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cell&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&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;"Widgets have length &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DEFAULT_LENGTH_MM&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;mm unless specified below"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So, I’ve achieved my aim: a guard against the original problem of having multiple sources of information that could potentially be out of sync. But I’ve done it using a simple test, rather than a more complex and fragile “derive” that wouldn’t have worked well anyway.&lt;/p&gt;
&lt;p&gt;By the way, for this specific project – &lt;a class="reference external" href="https://lukeplant.me.uk/firma-job/"&gt;we’re looking for another contract developer&lt;/a&gt;! It’s a very worthwhile project, and one I’m really enjoying – a small flexible team, with plenty of problem solving and fun challenges, so if you’re a talented developer and interested give me a shout.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="example-2-defining-ui-behaviour-for-domain-objects"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-6" role="doc-backlink"&gt;Example 2 - defining UI behaviour for domain objects&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Suppose you have a database that stores information about some kind of entity, like customers say, and you have different types of customer, represented using an enum of some kind, perhaps a string enum like this in Python:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-1" name="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-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_f2b4a70a5c30477db19bcb664ad3ee0d-2" name="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-3" name="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-3"&gt;&lt;/a&gt;
&lt;a id="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-4" name="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerType&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_f2b4a70a5c30477db19bcb664ad3ee0d-5" name="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;ENTERPRISE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enterprise"&lt;/span&gt;
&lt;a id="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-6" name="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;SMALL_FRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Small fry"&lt;/span&gt;  &lt;span class="c1"&gt;# Let’s be honest! Try not to let the name leak…&lt;/span&gt;
&lt;a id="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-7" name="rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_f2b4a70a5c30477db19bcb664ad3ee0d-7"&gt;&lt;/a&gt;    &lt;span class="n"&gt;LEGACY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Legacy"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We need to a way edit the different customer types, and they are sufficiently different that we want quite different interfaces. So, we might have a dictionary mapping the customer type to a function or class that defines the UI. If this were a Django project, it might be a different &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/forms/api/"&gt;Form&lt;/a&gt; class for each type:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_816ef8b1cda24e749756fb7df504b1d3-1" name="rest_code_816ef8b1cda24e749756fb7df504b1d3-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_816ef8b1cda24e749756fb7df504b1d3-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;CUSTOMER_EDIT_FORMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_816ef8b1cda24e749756fb7df504b1d3-2" name="rest_code_816ef8b1cda24e749756fb7df504b1d3-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_816ef8b1cda24e749756fb7df504b1d3-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;CustomerType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ENTERPRISE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EnterpriseCustomerForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_816ef8b1cda24e749756fb7df504b1d3-3" name="rest_code_816ef8b1cda24e749756fb7df504b1d3-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_816ef8b1cda24e749756fb7df504b1d3-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;CustomerType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SMALL_FRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SmallFryCustomerForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_816ef8b1cda24e749756fb7df504b1d3-4" name="rest_code_816ef8b1cda24e749756fb7df504b1d3-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_816ef8b1cda24e749756fb7df504b1d3-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;CustomerType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LEGACY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LegacyCustomerForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_816ef8b1cda24e749756fb7df504b1d3-5" name="rest_code_816ef8b1cda24e749756fb7df504b1d3-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_816ef8b1cda24e749756fb7df504b1d3-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, the DRY instinct kicks in and we notice that we now have two things we have to remember to keep in sync — any addition to the customer enum requires a corresponding addition to the UI definition dictionary. Maybe there are multiple dictionaries like this.&lt;/p&gt;
&lt;p&gt;We could attempt to solve this by “deriving”, or some “correct by construction” mechanism that puts the creation of a new customer type all in one place.&lt;/p&gt;
&lt;p&gt;For example, maybe we’ll have a base &lt;code class="docutils literal"&gt;Customer&lt;/code&gt; class with &lt;code class="docutils literal"&gt;get_edit_form_class()&lt;/code&gt; as an &lt;a class="reference external" href="https://docs.python.org/3/library/abc.html#abc.abstractmethod"&gt;abstractmethod&lt;/a&gt;, which means it is required to be implemented. If I fail to implement it in a subclass, I can’t even construct an instance of the new customer subclass – it will throw an error.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-1" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;abstractmethod&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-2" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-3" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-4" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-4"&gt;&lt;/a&gt;    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-5" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-5"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_edit_form_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-6" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-6"&gt;&lt;/a&gt;        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-7" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-8" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-9" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-9" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-9"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnterpriseCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-10" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-10" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-10"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_edit_form_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-11" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-11" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-11"&gt;&lt;/a&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EnterpriseCustomerForm&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-12" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-12" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-12"&gt;&lt;/a&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-13" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-13" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-13"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegacyCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_195f4e75bfff416fbcc0a0768ce11092-14" name="rest_code_195f4e75bfff416fbcc0a0768ce11092-14" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_195f4e75bfff416fbcc0a0768ce11092-14"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# etc.&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I still need my enum value, or at least a list of valid values that I can use for my database field. Maybe I could derive that automatically by looking at all the sublclasses?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ccf993fc5f1442fab52634d62a28d8cb-1" name="rest_code_ccf993fc5f1442fab52634d62a28d8cb-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ccf993fc5f1442fab52634d62a28d8cb-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;CUSTOMER_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;a id="rest_code_ccf993fc5f1442fab52634d62a28d8cb-2" name="rest_code_ccf993fc5f1442fab52634d62a28d8cb-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ccf993fc5f1442fab52634d62a28d8cb-2"&gt;&lt;/a&gt;    &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CUSTOMER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_ccf993fc5f1442fab52634d62a28d8cb-3" name="rest_code_ccf993fc5f1442fab52634d62a28d8cb-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ccf993fc5f1442fab52634d62a28d8cb-3"&gt;&lt;/a&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__subclasses__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_ccf993fc5f1442fab52634d62a28d8cb-4" name="rest_code_ccf993fc5f1442fab52634d62a28d8cb-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ccf993fc5f1442fab52634d62a28d8cb-4"&gt;&lt;/a&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or maybe an &lt;code class="docutils literal"&gt;__init_subclass__&lt;/code&gt; trick, and I can perhaps also set up the various mappings I’ll need that way?&lt;/p&gt;
&lt;p&gt;It’s at this point you should stop and think. In addition to requiring you to mix UI concerns into the &lt;code class="docutils literal"&gt;Customer&lt;/code&gt; class definitions, it’s getting complex and magical.&lt;/p&gt;
&lt;p&gt;The alternative I’m suggesting is this: require manual syncing of the two parts of the code base, but add a test to ensure that you did it. All you need is a few lines after your &lt;code class="docutils literal"&gt;CUSTOMER_EDIT_FORMS&lt;/code&gt; definition:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ddca5594de2547bba70fcb7d8ce5d642-1" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;CUSTOMER_EDIT_FORMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_ddca5594de2547bba70fcb7d8ce5d642-2" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-2"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# etc as before&lt;/span&gt;
&lt;a id="rest_code_ddca5594de2547bba70fcb7d8ce5d642-3" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_ddca5594de2547bba70fcb7d8ce5d642-4" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_ddca5594de2547bba70fcb7d8ce5d642-5" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-5"&gt;&lt;/a&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CustomerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_ddca5594de2547bba70fcb7d8ce5d642-6" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-6"&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_ddca5594de2547bba70fcb7d8ce5d642-7" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-7"&gt;&lt;/a&gt;        &lt;span class="n"&gt;c_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CUSTOMER_EDIT_FORMS&lt;/span&gt;
&lt;a id="rest_code_ddca5594de2547bba70fcb7d8ce5d642-8" name="rest_code_ddca5594de2547bba70fcb7d8ce5d642-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_ddca5594de2547bba70fcb7d8ce5d642-8"&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;"You've defined a new customer type &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, you need to add an entry in CUSTOMER_EDIT_FORMS"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You could do this as a more traditional unit test in a separate file, but for simple things like this, I think an assertion right next to the code works much better. It really helps local reasoning to be able to look and immediately conclude “yes, I can see that this dictionary must be exhaustive because the assertion tells me so.” Plus you get really early failure – as soon as you import the code.&lt;/p&gt;
&lt;p&gt;This kind of thing crops up a lot – if you create a class here, you’ve got to create another one over there, or add a dictionary entry etc. In these cases, I’m finding simple tests and assertions have a ton of advantages when compared to clever architectural contortions (or other things like advanced static typing gymnastics):&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;they are massively simpler to create and understand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you can write your own error message in the assertion. If you make a habit of using really clear error messages, like the one above, your code base will literally tell you how to maintain it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you can easily add things like exceptions. “Every Customer type needs an edit UI defined, except Legacy because they are read only” is an easy, small change to the above.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;This contrasts with cleverer mechanisms, which might require relaxing other constraints to the point where you defeat the whole point of the mechanism, or create more difficulties for yourself.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the rule about how the code works is very explicit, rather than implicit in some complicated code structure, and typically needs no comment other than what you write in the assertion message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you express and enforce the rule, with any complexities it gains, in just one place. Ironically, if you try to enforce this kind of constraint using type systems or hierarchies to eliminate repetition or the need for any kind of code syncing, you may find that when you come to change the constraint it actually requires touching far more places.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;temporarily silencing the assertion while developing is easy and doesn’t have far reaching consequences.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course, there are many times when being able to automatically derive things at the code level, including some complex relationships between parts of the code, can be a win, and it’s the kind of thing you can do in Python with its many powerful techniques.&lt;/p&gt;
&lt;p&gt;But my point is that you should remember the alternative: “synchronise manually, and have a test to check you did it.” Being able to add any kind of executable code at module level – the same level as class/function/constant definitions – is a Python super-power that you should use.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="example-3-external-polymorphism-and-static-typing"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-7" role="doc-backlink"&gt;Example 3 - external polymorphism and static typing&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A variant of the above problem is when, instead of an enum defining different types, I’ve got a set of classes that all need some behaviour defined.&lt;/p&gt;
&lt;p&gt;Often we just use polymorphism where a base class defines the methods or interfaces needed and sub-classes provide the implementation. However, as in the previous case, this can involve mixing concerns e.g. user interface code, possibly of several types, is mixed up with the base domain objects. It also imposes constraints on class hierarchies.&lt;/p&gt;
&lt;p&gt;Recently for these kind of cases, I’m more likely to prefer &lt;a class="reference external" href="https://wiki.c2.com/?ExternalPolymorphism"&gt;external polymorphism&lt;/a&gt; to avoid these problems. To give an example, in my current project I’m using the &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Command_pattern"&gt;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; extensively, and it involves manipulating CAM objects using a series of command objects that look something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-1" name="rest_code_91a516cbd9754ed486115efe1a1b562f-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-1"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-2" name="rest_code_91a516cbd9754ed486115efe1a1b562f-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeleteFeature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-3" name="rest_code_91a516cbd9754ed486115efe1a1b562f-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-4" name="rest_code_91a516cbd9754ed486115efe1a1b562f-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-5" name="rest_code_91a516cbd9754ed486115efe1a1b562f-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-6" name="rest_code_91a516cbd9754ed486115efe1a1b562f-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-6"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-7" name="rest_code_91a516cbd9754ed486115efe1a1b562f-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-7"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SetParameter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-8" name="rest_code_91a516cbd9754ed486115efe1a1b562f-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-8"&gt;&lt;/a&gt;    &lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-9" name="rest_code_91a516cbd9754ed486115efe1a1b562f-9" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-9"&gt;&lt;/a&gt;    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-10" name="rest_code_91a516cbd9754ed486115efe1a1b562f-10" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-10"&gt;&lt;/a&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-11" name="rest_code_91a516cbd9754ed486115efe1a1b562f-11" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-11"&gt;&lt;/a&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-12" name="rest_code_91a516cbd9754ed486115efe1a1b562f-12" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-12"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-13" name="rest_code_91a516cbd9754ed486115efe1a1b562f-13" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-13"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SetTextSegment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-14" name="rest_code_91a516cbd9754ed486115efe1a1b562f-14" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-14"&gt;&lt;/a&gt;    &lt;span class="n"&gt;text_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-15" name="rest_code_91a516cbd9754ed486115efe1a1b562f-15" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-15"&gt;&lt;/a&gt;    &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-16" name="rest_code_91a516cbd9754ed486115efe1a1b562f-16" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-16"&gt;&lt;/a&gt;    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-17" name="rest_code_91a516cbd9754ed486115efe1a1b562f-17" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-17"&gt;&lt;/a&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-18" name="rest_code_91a516cbd9754ed486115efe1a1b562f-18" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-18"&gt;&lt;/a&gt;
&lt;a id="rest_code_91a516cbd9754ed486115efe1a1b562f-19" name="rest_code_91a516cbd9754ed486115efe1a1b562f-19" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_91a516cbd9754ed486115efe1a1b562f-19"&gt;&lt;/a&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypeAlias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeleteFeature&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SetParameter&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SetTextSegment&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that none of them share a base class, but I do have a union type that gives me the complete set.&lt;/p&gt;
&lt;p&gt;It’s much more convenient to define the behaviour associated with these separately from these definitions, and so I have multiple other places that deal with &lt;code class="docutils literal"&gt;Command&lt;/code&gt;, such as the place that executes these commands and several others. One example that requires very little code to show is where I’m generating user-presentable tables that show groups of commands. I convert each of these &lt;code class="docutils literal"&gt;Command&lt;/code&gt; objects into key-value pairs that are used for column headings and values:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-1" name="rest_code_af78b50857a4473291cd264c74a94233-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_command_display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-2" name="rest_code_af78b50857a4473291cd264c74a94233-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-3" name="rest_code_af78b50857a4473291cd264c74a94233-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-3"&gt;&lt;/a&gt;        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;DeleteFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-4" name="rest_code_af78b50857a4473291cd264c74a94233-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-4"&gt;&lt;/a&gt;            &lt;span class="k"&gt;return&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;"Delete &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-5" name="rest_code_af78b50857a4473291cd264c74a94233-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-5"&gt;&lt;/a&gt;        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;SetParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-6" name="rest_code_af78b50857a4473291cd264c74a94233-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-6"&gt;&lt;/a&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-7" name="rest_code_af78b50857a4473291cd264c74a94233-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-7"&gt;&lt;/a&gt;        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;SetTextSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_af78b50857a4473291cd264c74a94233-8" name="rest_code_af78b50857a4473291cd264c74a94233-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_af78b50857a4473291cd264c74a94233-8"&gt;&lt;/a&gt;            &lt;span class="k"&gt;return&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;text_name&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;segment&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is giving me a similar problem to the one I had before I had before: if I add a new &lt;code class="docutils literal"&gt;Command&lt;/code&gt;, I have to remember to add the new branch to &lt;code class="docutils literal"&gt;get_command_display&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I could split out &lt;code class="docutils literal"&gt;get_command_display&lt;/code&gt; into a dictionary of functions, and apply the same technique as in the previous example, but it’s more work, a less natural fit for the problem and potentially less flexible.&lt;/p&gt;
&lt;p&gt;Instead, all I need to do is add &lt;a class="reference external" href="https://typing.readthedocs.io/en/latest/source/unreachable.html"&gt;exhaustiveness checking&lt;/a&gt; with one more branch:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_735be2e8636045ba985a8f5df028753b-1" name="rest_code_735be2e8636045ba985a8f5df028753b-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_735be2e8636045ba985a8f5df028753b-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_735be2e8636045ba985a8f5df028753b-2" name="rest_code_735be2e8636045ba985a8f5df028753b-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_735be2e8636045ba985a8f5df028753b-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# etc&lt;/span&gt;
&lt;a id="rest_code_735be2e8636045ba985a8f5df028753b-3" name="rest_code_735be2e8636045ba985a8f5df028753b-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_735be2e8636045ba985a8f5df028753b-3"&gt;&lt;/a&gt;    &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_735be2e8636045ba985a8f5df028753b-4" name="rest_code_735be2e8636045ba985a8f5df028753b-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_735be2e8636045ba985a8f5df028753b-4"&gt;&lt;/a&gt;        &lt;span class="n"&gt;assert_never&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, pyright will check that I didn’t forget to add branches here for any new &lt;code class="docutils literal"&gt;Command&lt;/code&gt;. The error message is not controllable, in contrast to hand-written asserts, but it is clear enough.&lt;/p&gt;
&lt;p&gt;The theme here is that additions in one part of the code require synchronised additions in other parts of the code, rather than being automatically correct “by construction”, but you have something that tests you didn’t forget.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="example-4-generated-code"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-8" role="doc-backlink"&gt;Example 4 - generated code&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In web development, ensuring consistent design and keeping different things in sync is a significant problem. There are many approaches, but let’s start with the simple case of using a single CSS stylesheet to define all the styles.&lt;/p&gt;
&lt;p&gt;We may want a bunch of components to have a consistent border colour, and a first attempt might look like this (ignoring the many issues of naming conventions here):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code css"&gt;&lt;a id="rest_code_649240c51cc04892a9df3fad356954da-1" name="rest_code_649240c51cc04892a9df3fad356954da-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_649240c51cc04892a9df3fad356954da-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;card-component&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;bordered-heading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_649240c51cc04892a9df3fad356954da-2" name="rest_code_649240c51cc04892a9df3fad356954da-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_649240c51cc04892a9df3fad356954da-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_649240c51cc04892a9df3fad356954da-3" name="rest_code_649240c51cc04892a9df3fad356954da-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_649240c51cc04892a9df3fad356954da-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This often becomes impractical when we want to organise by component, rather than by property, which introduces duplication:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code css"&gt;&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-1" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;card-component&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-2" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-3" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-4" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-5" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-5"&gt;&lt;/a&gt;&lt;span class="c"&gt;/* somewhere far away … */&lt;/span&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-6" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-7" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-7"&gt;&lt;/a&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;bordered-heading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-8" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-9" name="rest_code_5d97ce0ad15c486fa6f40b9b63125db0-9" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_5d97ce0ad15c486fa6f40b9b63125db0-9"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thankfully, CSS has variables, so the first application of “derive” is straightforward – we define a variable which we can use in multiple places:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code css"&gt;&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-1" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-2" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;--primary-border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-3" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-4" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-5" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-5"&gt;&lt;/a&gt;&lt;span class="c"&gt;/* elsewhere */&lt;/span&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-6" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-7" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-7"&gt;&lt;/a&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;bordered-heading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-8" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;solid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;--primary-border-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;a id="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-9" name="rest_code_b10626031d9f4ad5b26f3c49e1ff171a-9" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b10626031d9f4ad5b26f3c49e1ff171a-9"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;However, as the project grows, we may find that we want to use the same variables in different contexts where CSS isn’t applicable. So the next step at this point is typically to move to &lt;a class="reference external" href="https://css-tricks.com/what-are-design-tokens/"&gt;Design Tokens&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Practically speaking, this might mean that we now have our variables defined in a separate JSON file. Maybe something like this (using &lt;a class="reference external" href="https://design-tokens.github.io/community-group/format/#file-format"&gt;a W3C draft spec&lt;/a&gt;):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code json"&gt;&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-1" name="rest_code_b60059d5fc274bea9707d93d05553095-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-2" name="rest_code_b60059d5fc274bea9707d93d05553095-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"primary-border-color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-3" name="rest_code_b60059d5fc274bea9707d93d05553095-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-3"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#800000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-4" name="rest_code_b60059d5fc274bea9707d93d05553095-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-4"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"$type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color"&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-5" name="rest_code_b60059d5fc274bea9707d93d05553095-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-5"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-6" name="rest_code_b60059d5fc274bea9707d93d05553095-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-6"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"primary-hightlight-color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-7" name="rest_code_b60059d5fc274bea9707d93d05553095-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-7"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#FBC100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-8" name="rest_code_b60059d5fc274bea9707d93d05553095-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"$type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color"&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-9" name="rest_code_b60059d5fc274bea9707d93d05553095-9" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-9"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_b60059d5fc274bea9707d93d05553095-10" name="rest_code_b60059d5fc274bea9707d93d05553095-10" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_b60059d5fc274bea9707d93d05553095-10"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From this, we can automatically generate CSS fragments that contain the same variables quite easily – for simple cases, this isn’t more than a 50 line Python script.&lt;/p&gt;
&lt;p&gt;However, we’ve got some choices when it comes to how we put everything together. I think the general assumption in web development world is that a fully automatic “derive” is the only acceptable answer. This typically means you have to put your own CSS in a separate file, and then you have a build tool that watches for changes, and compiles your CSS plus the generated CSS into the final output that gets sent to the browser.&lt;/p&gt;
&lt;p&gt;In addition, once you’ve bought into these kind of tools you’ll find they want to do extensive changes to the output, and define more and more extensions to the underlying languages. For example, &lt;a class="reference external" href="https://www.npmjs.com/package/@csstools/postcss-design-tokens"&gt;postcss-design-tokens&lt;/a&gt; wants you to write things like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code css"&gt;&lt;a id="rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-1" name="rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;foo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-2" name="rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;design-token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'color.background.primary'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;a id="rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-3" name="rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_c177bea80fbf4f9eb4516dcefcefe9fd-3"&gt;&lt;/a&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And instead of using CSS variables in the output, it puts the value of the token right in to every place in your code that uses it.&lt;/p&gt;
&lt;p&gt;This approach has various problems, in particular that you become more and more dependent on the build process, and the output gets further from your input. You can no longer use the &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools"&gt;Dev Tools&lt;/a&gt; built in to your browser to do editing – the flow of using Dev Tools to experiment with changing a single spacing or colour CSS variable for global changes is broken, you need your build tool. And you can’t easily copy changes from Dev Tools back into the source, because of the transformation step, and debugging can be similarly difficult. And then, you’ll probably want special IDE support for the special CSS extensions, rather than being able to lean on your editor simply understanding CSS, and any other tools that want to look at your CSS now need support etc.&lt;/p&gt;
&lt;p&gt;It’s also a lot of extra infrastructure and complexity to solve this one problem, especially when our design tokens JSON file is probably not going to change that often, or is going to have long periods of high stability. There are good reasons to want to be essentially &lt;a class="reference external" href="https://world.hey.com/dhh/you-can-t-get-faster-than-no-build-7a44131c"&gt;build free&lt;/a&gt;. The current state of the art in this space is that &lt;a class="reference external" href="https://vitejs.dev/guide/features#css"&gt;to get your build tool to compile your CSS&lt;/a&gt; you add &lt;code class="docutils literal"&gt;import &lt;span class="pre"&gt;'./styles.css'&lt;/span&gt;&lt;/code&gt; &lt;strong&gt;in your entry point Javascript file!&lt;/strong&gt; What if I don’t even have a Javascript file? I think I understand how this sort of thing came about, but don’t try to tell me that it’s anything less than completely bonkers.&lt;/p&gt;
&lt;p&gt;Do we have an alternative to the fully automatic derive?&lt;/p&gt;
&lt;p&gt;Using the “test” approach, we do. We can even stick with our single CSS file – we just write it like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code css"&gt;&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-1" name="rest_code_37c3c2a15d1a4332be2d9319390962da-1" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-1"&gt;&lt;/a&gt;&lt;span class="c"&gt;/* DESIGN TOKENS START */&lt;/span&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-2" name="rest_code_37c3c2a15d1a4332be2d9319390962da-2" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-2"&gt;&lt;/a&gt;&lt;span class="c"&gt;/* auto-created block - do not edit */&lt;/span&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-3" name="rest_code_37c3c2a15d1a4332be2d9319390962da-3" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-4" name="rest_code_37c3c2a15d1a4332be2d9319390962da-4" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-4"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;--primary-border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#800000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-5" name="rest_code_37c3c2a15d1a4332be2d9319390962da-5" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-5"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;--primary-highlight-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#FBC100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-6" name="rest_code_37c3c2a15d1a4332be2d9319390962da-6" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-6"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-7" name="rest_code_37c3c2a15d1a4332be2d9319390962da-7" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-7"&gt;&lt;/a&gt;&lt;span class="c"&gt;/* DESIGN TOKENS END */&lt;/span&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-8" name="rest_code_37c3c2a15d1a4332be2d9319390962da-8" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_37c3c2a15d1a4332be2d9319390962da-9" name="rest_code_37c3c2a15d1a4332be2d9319390962da-9" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#rest_code_37c3c2a15d1a4332be2d9319390962da-9"&gt;&lt;/a&gt;&lt;span class="c"&gt;/* the rest of our CSS here */&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The contents of this block will be almost certainly auto-generated. We won’t have a process that fully automatically updates it, however, because this is the same file where we are putting our custom CSS, and we don’t want any possibility of lost work due to the file being overwritten as we are editing it.&lt;/p&gt;
&lt;p&gt;On the other hand we don’t want things to get out of sync, so we’ll add a test that checks whether the current &lt;code class="docutils literal"&gt;styles.css&lt;/code&gt; contains the block of design tokens that we expect to be there, based on the JSON. For actually updating the block, we’ll need some kind of manual step – maybe a script that can find and update the &lt;code class="docutils literal"&gt;DESIGN TOKEN START&lt;/code&gt; block, maybe &lt;a class="reference external" href="https://cog.readthedocs.io/en/latest/"&gt;cog&lt;/a&gt; – which is a perfect little tool for this use case — or we could just copy-paste.&lt;/p&gt;
&lt;p&gt;There are also slightly simpler solutions in this case, like using a &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/@import"&gt;CSS import&lt;/a&gt; if you don’t mind having multiple CSS files.&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/keeping-things-in-sync-derive-vs-test/#toc-entry-9" role="doc-backlink"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For all the examples above, the solutions I’ve presented might not work perfectly for your context. You might also want to draw the line at different place to me. But my main point is that we don’t have to go all the way with a fully automatic derive solution to eliminate any manual syncing. Having some manual work plus a mechanism to test that two things are in sync is a perfectly legitimate solution, and it can avoid some of the large costs that come with structuring everything around “derive”.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="links"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/keeping-things-in-sync-derive-vs-test/#toc-entry-10" role="doc-backlink"&gt;Links&lt;/a&gt;&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://www.jmduke.com/posts/weird-tests-tacit-knowledge.html"&gt;Use weird tests to capture tacit knowledge&lt;/a&gt;: this has a similar idea – the ideal case would be that the tacit knowledge is unnecessary because the system is correct “by construction” or automation; but failing that, you can have a test to ensure things aren’t forgotten.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://lobste.rs/s/pqhwph/keeping_things_sync_derive_vs_test"&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="django" label="Django"/>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
    <category term="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>Never, Sometimes, Always</title>
    <id>https://lukeplant.me.uk/blog/posts/never-sometimes-always/</id>
    <updated>2024-06-14T10:00:11+01:00</updated>
    <published>2024-06-14T10:00:11+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/never-sometimes-always/"/>
    <summary type="html">&lt;p&gt;Just like the only numbers programmers care about are zero, one, infinity, the only frequencies we care about are Never, Sometimes and Always.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In software development, we often use the &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Zero_one_infinity_rule"&gt;Zero one infinity&lt;/a&gt; rule (or “zero one many”) to decide how many instances of things we should allow.&lt;/p&gt;
&lt;p&gt;For example, a customer record in your database might have zero email addresses associated with it, one email address, or many email addresses. If you currently support one and someone says they need two, you should skip two and go straight to infinity. Adding in support for just a second email address is a bad idea, because you will still have to cope with a variable number (one or two), so it is actually simpler to cope with any number, plus you are future proof.&lt;/p&gt;
&lt;p&gt;There is a parallel to frequencies of events: just as programmers only care about the numbers 0, 1 and ∞, the only three frequencies we care about are Never, Sometimes, and Always.&lt;/p&gt;
&lt;p&gt;I’ve often had conversations with a client that go like this:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Me: is it possible that situation X will arise?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Client: No, no, no. That doesn’t happen. Hardly ever.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem is that Hardly Ever is still Sometimes, so the client’s response has gone from being a definite “no” to a definite “yes” in less than a second. It doesn’t matter to me that it &lt;strong&gt;rarely&lt;/strong&gt; happens – the situation still happens, and I’ve got to write code to cope with it. The fact that the code won’t be used very often doesn’t make it cheaper to write – it’s not like engineering where a machine that is used less will require less maintenance.&lt;/p&gt;
&lt;p&gt;The Hardly Ever cases can in fact be significantly more costly to cope with, because, for example, it might be harder to find or construct examples to use for testing. If a special branch of code is constructed to handle the rare situation, such code is likely to be less well tested and more buggy, and cost more in the long run than the code which handles the common case.&lt;/p&gt;
&lt;p&gt;This kind of thing often comes up with a client when converting from a manual system in which all input is being handled by an intelligent agent, so Hardly Ever doesn’t cause too many problems, because the person just does something sensible. So to the client it might feel like I’m being overly pedantic and focusing all my energy on the weird cases – but in converting to the computerised system, we don’t have an intelligent agent behind the wheel, so every case has got to be covered off. Understanding the weird cases as early as possible actually helps me come up with a design in which those cases are not exceptions at all, they are just normal operation and require zero additional code – and this makes the design a lot more robust.&lt;/p&gt;
&lt;p&gt;In fact, Hardly Ever means that the frequency is relatively high – it probably means that you’ve witnessed at least one case of it in the past, so that’s a definite Sometimes. More slippery cases are things like It’s Never Happened Yet, which just means you haven’t personally seen an instance of it, but there’s no theoretical reason why it couldn’t happen. In other words, Sometimes. And then there is It’ll Never Happen, at which point my spidey sense is saying “it’s going to happen, and probably sooner than we expect”. So, again, it’s a Sometimes.&lt;/p&gt;
&lt;section id="caveats"&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;There are, of course, times when the programmer does care about relative frequency – where 1% is very different from 10% or 90% – and most of them fall under the term “optimisation”.&lt;/p&gt;
&lt;p&gt;If you are a programmer wearing a product manager’s hat, you are of course allowed to produce something that is an “80% solution”, or even a “20%” solution, knowing that your product simply won’t cope at all with some cases, for which people will need to find other solutions. You are optimising for a common case at the level of business needs, and you’ll want to know what that common case is.&lt;/p&gt;
&lt;p&gt;You may also care about relative frequency for other kinds of optimisation, such as performance or user interface design. You are allowed to have an interface which works great for the typical case but is more clunky for the exceptional one, for example.&lt;/p&gt;
&lt;p&gt;Even here there are dangers though. Suppose you optimise an algorithm for what happens 99% of the time, so that you get, let’s say &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Time_complexity#Linear_time"&gt;linear time complexity&lt;/a&gt; for normal workloads. Unfortunately, for the 1% case you drop into a worse time complexity, like quadratic time. You still have to deal with the 1%, at which point your system may be brought to its knees. It may be bad enough that the code effectively does not work at all for the user. Or if we are talking about services you run, dealing with the 1% case ends up taking up so much CPU or memory that in terms of resources, your 1% case is actually 99% of the problem.&lt;/p&gt;
&lt;p&gt;In addition, if you are running in a hostile environment like the internet, an attacker may be able to force the worst case performance, and now you have a &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Denial-of-service_attack"&gt;Denial of service vulnerability&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You may also have qualitative as well as quantitative differences in the way that the code copes with the “rarely” cases. At the very bottom end it might be to add an assertion that will fail and crash the process if the unlikely thing happens, assuming that this will not be a critical problem and you’ll get notified. Then, once you start getting those notifications, you can assess whether it is worth devoting more resources to.&lt;/p&gt;
&lt;p&gt;Or, you might have slightly more graceful handling, and perhaps a manual override to deal with the exceptional case – if such a thing is possible, and isn’t itself more work than just dealing with it in the code.&lt;/p&gt;
&lt;p&gt;A final caveat I can think of is that there is always some cut-off at which you count extremely unlikely events as Never. For example, the odds of a collision in a 256-bit hash function like &lt;a class="reference external" href="https://en.wikipedia.org/wiki/SHA-2"&gt;SHA-256&lt;/a&gt; are approximately &lt;a class="reference external" href="https://crypto.stackexchange.com/questions/39641/what-are-the-odds-of-collisions-for-a-hash-function-with-256-bit-output/39644#39644"&gt;one in a gazillion&lt;/a&gt;. That’s technically Sometimes, but it’s pretty reasonable to count as Never.&lt;/p&gt;
&lt;p&gt;Of course, that’s what they probably said about &lt;a class="reference external" href="https://en.wikipedia.org/wiki/MD5"&gt;MD5&lt;/a&gt; as well…&lt;/p&gt;
&lt;/section&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/jgtmfm/never_sometimes_always"&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="software-development" label="Software development"/>
  </entry>
  <entry>
    <title>pyastgrep and custom linting</title>
    <id>https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/</id>
    <updated>2024-05-23T20:07:34+01:00</updated>
    <published>2024-05-23T20:07:34+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/"/>
    <summary type="html">&lt;p&gt;Methodology and sample code for using pyastgrep to do custom linting tasks on Python source code.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;A while back I released &lt;a class="reference external" href="https://github.com/spookylukey/pyastgrep/"&gt;pyastgrep&lt;/a&gt;, which is a rewrite of &lt;a class="reference external" href="https://github.com/hchasestevens/astpath"&gt;astpath&lt;/a&gt;. It’s a tool that allows you to search for specific Python syntax elements using &lt;a class="reference external" href="https://en.wikipedia.org/wiki/XPath"&gt;XPath&lt;/a&gt; as a query language.&lt;/p&gt;
&lt;p&gt;As part of the rewrite, I separated out the layers of code so that it can now be used &lt;a class="reference external" href="https://pyastgrep.readthedocs.io/en/latest/library.html"&gt;as a library&lt;/a&gt; as well as a command line tool. I haven’t committed to very much API surface area for library usage, but there is enough.&lt;/p&gt;
&lt;p&gt;My main personal use of this has been for linting tasks or enforcing of conventions that might be difficult to do otherwise. I don’t always use this – quite often I’d reach for &lt;a class="reference external" href="https://semgrep.dev/docs/writing-rules/rule-ideas"&gt;custom Semgrep rules&lt;/a&gt;, and at other times &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/"&gt;I use introspection to enforce conventions&lt;/a&gt;. However, there are times when both of these fail or are rather difficult.&lt;/p&gt;
&lt;section id="examples"&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;p&gt;Some examples of the kinds of rules I’m thinking of include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Boolean arguments to functions/methods should always be “keyword only”.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/keyword-only-arguments-in-python/"&gt;Keyword-only arguments are a big win in many cases&lt;/a&gt;, and especially when it
comes to boolean values. For example, forcing &lt;code class="docutils literal"&gt;delete_thing(True, False)&lt;/code&gt;
to be something like &lt;code class="docutils literal"&gt;delete_thing(permanent=True, force=False)&lt;/code&gt; is an easy
win, and this is common enough that applying this as a default policy across
the code base will probably be a good idea.&lt;/p&gt;
&lt;p&gt;The pattern can be distinguished easily at syntax level. Good:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ea88bed58972461888e3f7740fb983b6-1" name="rest_code_ea88bed58972461888e3f7740fb983b6-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_ea88bed58972461888e3f7740fb983b6-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&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;my_bool_arg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_ea88bed58972461888e3f7740fb983b6-2" name="rest_code_ea88bed58972461888e3f7740fb983b6-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_ea88bed58972461888e3f7740fb983b6-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_5724f37ed5dc4b33951ffaffee8fff33-1" name="rest_code_5724f37ed5dc4b33951ffaffee8fff33-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_5724f37ed5dc4b33951ffaffee8fff33-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_bool_arg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_5724f37ed5dc4b33951ffaffee8fff33-2" name="rest_code_5724f37ed5dc4b33951ffaffee8fff33-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_5724f37ed5dc4b33951ffaffee8fff33-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simple coding conventions like “Don’t use single letter variables like &lt;code class="docutils literal"&gt;i&lt;/code&gt; or &lt;code class="docutils literal"&gt;j&lt;/code&gt; as a loop variables, use &lt;code class="docutils literal"&gt;index&lt;/code&gt; or &lt;code class="docutils literal"&gt;idx&lt;/code&gt; instead”.&lt;/p&gt;
&lt;p&gt;This can be found by looking for code like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_a370f0f8d6fb425ab3f47fcdca7edda2-1" name="rest_code_a370f0f8d6fb425ab3f47fcdca7edda2-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_a370f0f8d6fb425ab3f47fcdca7edda2-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&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="nb"&gt;enumerate&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_a370f0f8d6fb425ab3f47fcdca7edda2-2" name="rest_code_a370f0f8d6fb425ab3f47fcdca7edda2-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_a370f0f8d6fb425ab3f47fcdca7edda2-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You might not care about this, but if you do, you really want the rule to be applied as an automated test, not a nit-picky code review.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Django-specific one: for &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#howto-custom-template-tags-inclusion-tags"&gt;inclusion tags&lt;/a&gt;,
the tag names should match the template file name. This is nice for
consistency and code navigation, plus I actually have some custom “jump to
definition” code in my editor that relies on it for fast navigation.&lt;/p&gt;
&lt;p&gt;The pattern can again be seen quite easily at the syntax level. Good:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_7e7978173683430195fbdc72c1aa528e-1" name="rest_code_7e7978173683430195fbdc72c1aa528e-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_7e7978173683430195fbdc72c1aa528e-1"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@inclusion_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"something/foo.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7e7978173683430195fbdc72c1aa528e-2" name="rest_code_7e7978173683430195fbdc72c1aa528e-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_7e7978173683430195fbdc72c1aa528e-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_7e7978173683430195fbdc72c1aa528e-3" name="rest_code_7e7978173683430195fbdc72c1aa528e-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_7e7978173683430195fbdc72c1aa528e-3"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_a1da7ac1abde479688940ca971a960b4-1" name="rest_code_a1da7ac1abde479688940ca971a960b4-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_a1da7ac1abde479688940ca971a960b4-1"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@inclusion_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"something/bar.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_a1da7ac1abde479688940ca971a960b4-2" name="rest_code_a1da7ac1abde479688940ca971a960b4-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_a1da7ac1abde479688940ca971a960b4-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_a1da7ac1abde479688940ca971a960b4-3" name="rest_code_a1da7ac1abde479688940ca971a960b4-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_a1da7ac1abde479688940ca971a960b4-3"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any ’task’ (something decorated with &lt;code class="docutils literal"&gt;@task&lt;/code&gt;) should be named &lt;code class="docutils literal"&gt;foo_task&lt;/code&gt;
in order to give a clue that it works as an asynchronous call, and its return
value is just a promise object.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are many more examples you’ll come up with once you start thinking like this.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="method"&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;p&gt;Having identified the bad patterns we want to find and fix, my method for doing so looks as follows. It contains a number of tips and refinements I’ve made over the past few years.&lt;/p&gt;
&lt;p&gt;First, I open a test file, e.g. &lt;code class="docutils literal"&gt;tests/test_conventions.py&lt;/code&gt;, and start by inserting some example code – at least one bad example (the kind we are trying to fix), and one good example.&lt;/p&gt;
&lt;p&gt;There are a few reasons for this:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;First, I need to make sure &lt;a class="reference external" href="https://www.johndcook.com/blog/2018/05/26/proving-life-on-earth/"&gt;I can prove life exists on earth&lt;/a&gt;, as John D. Cook puts it. I’ll say more about this later on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, it gives me a deliberately simplified bit of code that I can pass to &lt;a class="reference external" href="https://pyastgrep.readthedocs.io/en/latest/usage.html#understanding-the-xml-structure"&gt;pyastdump&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third, it provides some explanation for the test I’m going to write, and a potentially rather hairy XPath expression.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ll use my first example above, keyword-only boolean args. I start by inserting the following text into my test file:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-1" name="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_boolean_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-2" name="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;a id="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-3" name="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-3"&gt;&lt;/a&gt;
&lt;a id="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-4" name="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-5" name="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-5"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;good_boolean_arg&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;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-6" name="rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_e8e9116d628d4dd6a4d8770ebd3a5e1a-6"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, I copy both of these in turn to the clipboard (or both together if there isn’t much code, like in this case), and pass them through &lt;code class="docutils literal"&gt;pyastdump&lt;/code&gt;. From a terminal, I do:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_2e1fa86350264bcd9b49b2ba293914eb-1" name="rest_code_2e1fa86350264bcd9b49b2ba293914eb-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_2e1fa86350264bcd9b49b2ba293914eb-1"&gt;&lt;/a&gt;$&lt;span class="w"&gt; &lt;/span&gt;xsel&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pyastdump&lt;span class="w"&gt; &lt;/span&gt;-
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I’m using the &lt;code class="docutils literal"&gt;xsel&lt;/code&gt; Linux utility, you can also use &lt;code class="docutils literal"&gt;xclip &lt;span class="pre"&gt;-out&lt;/span&gt;&lt;/code&gt;, or &lt;code class="docutils literal"&gt;pbpaste&lt;/code&gt; on MacOS, or &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;Get-Clipboard&lt;/span&gt;&lt;/code&gt; in Powershell.&lt;/p&gt;
&lt;p&gt;This gives me some AST to look at, structured as XML:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code xml"&gt;&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-1" name="rest_code_c802ca4824344216ab032084f8e1c1c2-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-1"&gt;&lt;/a&gt;&lt;span class="nt"&gt;&amp;lt;Module&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-2" name="rest_code_c802ca4824344216ab032084f8e1c1c2-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-3" name="rest_code_c802ca4824344216ab032084f8e1c1c2-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-3"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;FunctionDef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"str"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"bad_boolean_arg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-4" name="rest_code_c802ca4824344216ab032084f8e1c1c2-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-4"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-5" name="rest_code_c802ca4824344216ab032084f8e1c1c2-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-5"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;arguments&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-6" name="rest_code_c802ca4824344216ab032084f8e1c1c2-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-6"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;posonlyargs/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-7" name="rest_code_c802ca4824344216ab032084f8e1c1c2-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-7"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-8" name="rest_code_c802ca4824344216ab032084f8e1c1c2-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;arg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"str"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;arg=&lt;/span&gt;&lt;span class="s"&gt;"foo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-9" name="rest_code_c802ca4824344216ab032084f8e1c1c2-9" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-9"&gt;&lt;/a&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;annotation&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-10" name="rest_code_c802ca4824344216ab032084f8e1c1c2-10" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-10"&gt;&lt;/a&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"25"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"str"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bool"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-11" name="rest_code_c802ca4824344216ab032084f8e1c1c2-11" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-11"&gt;&lt;/a&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;ctx&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-12" name="rest_code_c802ca4824344216ab032084f8e1c1c2-12" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-12"&gt;&lt;/a&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Load/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-13" name="rest_code_c802ca4824344216ab032084f8e1c1c2-13" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-13"&gt;&lt;/a&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/ctx&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-14" name="rest_code_c802ca4824344216ab032084f8e1c1c2-14" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-14"&gt;&lt;/a&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-15" name="rest_code_c802ca4824344216ab032084f8e1c1c2-15" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-15"&gt;&lt;/a&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/annotation&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-16" name="rest_code_c802ca4824344216ab032084f8e1c1c2-16" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-16"&gt;&lt;/a&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/arg&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-17" name="rest_code_c802ca4824344216ab032084f8e1c1c2-17" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-17"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-18" name="rest_code_c802ca4824344216ab032084f8e1c1c2-18" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-18"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;kwonlyargs/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-19" name="rest_code_c802ca4824344216ab032084f8e1c1c2-19" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-19"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;kw_defaults/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-20" name="rest_code_c802ca4824344216ab032084f8e1c1c2-20" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-20"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;defaults/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-21" name="rest_code_c802ca4824344216ab032084f8e1c1c2-21" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-21"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-22" name="rest_code_c802ca4824344216ab032084f8e1c1c2-22" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-22"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-23" name="rest_code_c802ca4824344216ab032084f8e1c1c2-23" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-23"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-24" name="rest_code_c802ca4824344216ab032084f8e1c1c2-24" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-24"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-25" name="rest_code_c802ca4824344216ab032084f8e1c1c2-25" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-25"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-26" name="rest_code_c802ca4824344216ab032084f8e1c1c2-26" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-26"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;decorator_list/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-27" name="rest_code_c802ca4824344216ab032084f8e1c1c2-27" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-27"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/FunctionDef&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-28" name="rest_code_c802ca4824344216ab032084f8e1c1c2-28" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-28"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-29" name="rest_code_c802ca4824344216ab032084f8e1c1c2-29" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-29"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;type_ignores/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c802ca4824344216ab032084f8e1c1c2-30" name="rest_code_c802ca4824344216ab032084f8e1c1c2-30" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c802ca4824344216ab032084f8e1c1c2-30"&gt;&lt;/a&gt;&lt;span class="nt"&gt;&amp;lt;/Module&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this case, the current structure of Python’s AST has helped us out a lot – it has separated out &lt;code class="docutils literal"&gt;posonlyargs&lt;/code&gt; (positional only arguments), &lt;code class="docutils literal"&gt;args&lt;/code&gt; (positional or keyword), and &lt;code class="docutils literal"&gt;kwonlyargs&lt;/code&gt; (keyword only args). We can see the offending &lt;code class="docutils literal"&gt;annotation&lt;/code&gt; containing a &lt;code class="docutils literal"&gt;Name&lt;/code&gt; with &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;id="bool"&lt;/span&gt;&lt;/code&gt; inside the &lt;code class="docutils literal"&gt;args&lt;/code&gt;, when we want it only to be allowed as a keyword-only argument.&lt;/p&gt;
&lt;p&gt;(Do we want to disallow boolean-annotated arguments as positional only? I’m leaning towards “no” here, as positional only is quite rare and usually a very deliberate choice).&lt;/p&gt;
&lt;p&gt;I now have to construct an XPath expression that will find the offending XML nodes, but not match good examples. It’s pretty straightforward in this case, once you know the basics of XPath. I test it out straight away at the CLI:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_175f9eee0444430f8f01effbf5603b0c-1" name="rest_code_175f9eee0444430f8f01effbf5603b0c-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_175f9eee0444430f8f01effbf5603b0c-1"&gt;&lt;/a&gt;pyastgrep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'.//FunctionDef/args/arguments/args/arg/annotation/Name[@id="bool"]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tests/test_conventions.py
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If I’ve done it correctly, it should print my bad example, and not my good example.&lt;/p&gt;
&lt;p&gt;Then I widen the net, omitting &lt;code class="docutils literal"&gt;tests/test_conventions.py&lt;/code&gt; to search everywhere in my current directory.&lt;/p&gt;
&lt;p&gt;At this point, I’ve probably got some real results that I want to address, but I might also notice there are other variants of the same thing I need to be able to match, and so I iterate, adding more bad/good examples as necessary.&lt;/p&gt;
&lt;p&gt;Now I need to write a test. It’s going to look like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_238faac6cd994dc49d096ac3aaa745c1-1" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_boolean_arguments_are_keyword_only&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_238faac6cd994dc49d096ac3aaa745c1-2" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;assert_expected_pyastgrep_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_238faac6cd994dc49d096ac3aaa745c1-3" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-3"&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_238faac6cd994dc49d096ac3aaa745c1-4" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-4"&gt;&lt;/a&gt;&lt;span class="sd"&gt;        .//FunctionDef/args/arguments/args/arg/annotation/Name[@id="bool"]&lt;/span&gt;
&lt;a id="rest_code_238faac6cd994dc49d096ac3aaa745c1-5" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-5"&gt;&lt;/a&gt;&lt;span class="sd"&gt;        """&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_238faac6cd994dc49d096ac3aaa745c1-6" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-6"&gt;&lt;/a&gt;        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Function arguments with type `bool` should be keyword-only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_238faac6cd994dc49d096ac3aaa745c1-7" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-7"&gt;&lt;/a&gt;        &lt;span class="n"&gt;expected_count&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;a id="rest_code_238faac6cd994dc49d096ac3aaa745c1-8" name="rest_code_238faac6cd994dc49d096ac3aaa745c1-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_238faac6cd994dc49d096ac3aaa745c1-8"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Of course, the real work is being done inside my &lt;code class="docutils literal"&gt;assert_expected_pyastgrep_matches&lt;/code&gt; utility, which looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-1" name="rest_code_f237d5263be943e3bdaf27e7772ac980-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-2" name="rest_code_f237d5263be943e3bdaf27e7772ac980-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;boltons&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;iterutils&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-3" name="rest_code_f237d5263be943e3bdaf27e7772ac980-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyastgrep.api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search_python_files&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-4" name="rest_code_f237d5263be943e3bdaf27e7772ac980-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-5" name="rest_code_f237d5263be943e3bdaf27e7772ac980-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-5"&gt;&lt;/a&gt;&lt;span class="n"&gt;SRC_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# depends on project structure&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-6" name="rest_code_f237d5263be943e3bdaf27e7772ac980-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-7" name="rest_code_f237d5263be943e3bdaf27e7772ac980-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-8" name="rest_code_f237d5263be943e3bdaf27e7772ac980-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-8"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_expected_pyastgrep_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xpath_expr&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;span class="n"&gt;expected_count&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;message&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_f237d5263be943e3bdaf27e7772ac980-9" name="rest_code_f237d5263be943e3bdaf27e7772ac980-9" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-9"&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_f237d5263be943e3bdaf27e7772ac980-10" name="rest_code_f237d5263be943e3bdaf27e7772ac980-10" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-10"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Asserts that the pyastgrep XPath expression matches only `expected_count` times,&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-11" name="rest_code_f237d5263be943e3bdaf27e7772ac980-11" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-11"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    each of which must be marked with `pyastgrep_exception`&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-12" name="rest_code_f237d5263be943e3bdaf27e7772ac980-12" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-12"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-13" name="rest_code_f237d5263be943e3bdaf27e7772ac980-13" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-13"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    `message` is a message to be printed on failure.&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-14" name="rest_code_f237d5263be943e3bdaf27e7772ac980-14" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-14"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-15" name="rest_code_f237d5263be943e3bdaf27e7772ac980-15" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-15"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-16" name="rest_code_f237d5263be943e3bdaf27e7772ac980-16" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-16"&gt;&lt;/a&gt;    &lt;span class="n"&gt;xpath_expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xpath_expr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-17" name="rest_code_f237d5263be943e3bdaf27e7772ac980-17" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-17"&gt;&lt;/a&gt;    &lt;span class="n"&gt;matches&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;Match&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;item&lt;/span&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;search_python_files&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;SRC_ROOT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;xpath_expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&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;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-18" name="rest_code_f237d5263be943e3bdaf27e7772ac980-18" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-18"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-19" name="rest_code_f237d5263be943e3bdaf27e7772ac980-19" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-19"&gt;&lt;/a&gt;    &lt;span class="n"&gt;expected_matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other_matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iterutils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-20" name="rest_code_f237d5263be943e3bdaf27e7772ac980-20" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-20"&gt;&lt;/a&gt;        &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"pyastgrep: expected"&lt;/span&gt; &lt;span class="ow"&gt;in&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;matching_line&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-21" name="rest_code_f237d5263be943e3bdaf27e7772ac980-21" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-21"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-22" name="rest_code_f237d5263be943e3bdaf27e7772ac980-22" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-22"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-23" name="rest_code_f237d5263be943e3bdaf27e7772ac980-23" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-23"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&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;expected_matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;expected_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-24" name="rest_code_f237d5263be943e3bdaf27e7772ac980-24" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-24"&gt;&lt;/a&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="kc"&gt;False&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;"Expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; matches but found &lt;/span&gt;&lt;span class="si"&gt;{&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;expected_matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;xpath_expr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-25" name="rest_code_f237d5263be943e3bdaf27e7772ac980-25" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-25"&gt;&lt;/a&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-26" name="rest_code_f237d5263be943e3bdaf27e7772ac980-26" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-26"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;other_matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-27" name="rest_code_f237d5263be943e3bdaf27e7772ac980-27" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-27"&gt;&lt;/a&gt;        &lt;span class="n"&gt;message&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-28" name="rest_code_f237d5263be943e3bdaf27e7772ac980-28" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-28"&gt;&lt;/a&gt;        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; Failing examples:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-29" name="rest_code_f237d5263be943e3bdaf27e7772ac980-29" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-29"&gt;&lt;/a&gt;        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-30" name="rest_code_f237d5263be943e3bdaf27e7772ac980-30" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-30"&gt;&lt;/a&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;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineno&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;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col_offset&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;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matching_line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-31" name="rest_code_f237d5263be943e3bdaf27e7772ac980-31" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-31"&gt;&lt;/a&gt;            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;other_matches&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-32" name="rest_code_f237d5263be943e3bdaf27e7772ac980-32" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-32"&gt;&lt;/a&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_f237d5263be943e3bdaf27e7772ac980-33" name="rest_code_f237d5263be943e3bdaf27e7772ac980-33" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_f237d5263be943e3bdaf27e7772ac980-33"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There is a bit of explaining to do now.&lt;/p&gt;
&lt;p&gt;Being sure that you can “find life on earth” is especially important for a negative test like this. It would be very easy to have an XPath query that you thought worked but didn’t, as it might just silently return zero results. In addition, Python’s AST is not stable – so a query that works now might stop working in the future.&lt;/p&gt;
&lt;p&gt;It’s like you have a machine that claims to be able to find needles in haystacks – when it comes back and says “no needles found”, do you believe it? To increase your confidence that everything works and continues to work, you place a few needles at locations that you know, then check that the machine is able to find those needles. When it claims “found exactly 2 needles”, and you can account for those, you’ve got much more confidence that it has indeed found the only needles.&lt;/p&gt;
&lt;p&gt;So, it’s important to leave my bad examples in there.&lt;/p&gt;
&lt;p&gt;But, I obviously don’t want the bad examples to cause the test to fail! In addition, I want a mechanism for exceptions. A simple mechanism I’ve chosen is to add the text &lt;code class="docutils literal"&gt;pyastgrep: expected&lt;/code&gt; as a comment.&lt;/p&gt;
&lt;p&gt;So, I need to change my bad example like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_3983f4cddba240f8b3e123a330933346-1" name="rest_code_3983f4cddba240f8b3e123a330933346-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_3983f4cddba240f8b3e123a330933346-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_boolean_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# pyastgrep: expected&lt;/span&gt;
&lt;a id="rest_code_3983f4cddba240f8b3e123a330933346-2" name="rest_code_3983f4cddba240f8b3e123a330933346-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_3983f4cddba240f8b3e123a330933346-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I also pass &lt;code class="docutils literal"&gt;expected_count=1&lt;/code&gt; to indicate that I expect to find at least one bad example (or more, if I’ve added more bad examples).&lt;/p&gt;
&lt;p&gt;Hopefully that explains everything &lt;code class="docutils literal"&gt;assert_expected_pyastgrep_matches&lt;/code&gt; does. A couple more notes:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;it uses &lt;a class="reference external" href="https://boltons.readthedocs.io/en/latest/"&gt;boltons&lt;/a&gt;, a pretty useful set of Python utilities&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it requires a &lt;code class="docutils literal"&gt;SRC_ROOT&lt;/code&gt; folder to be defined, which will depend on your project, and might be different depending on which folder(s) you want to apply the convention too.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, everything is set up, and I run the test for real, hopefully locating all the bad usages. I work through them and fix, then leave the test in.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="tips"&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;pyastgrep works strictly at the syntax level, so unlike Semgrep you might get caught out by aliases if you try match on specific names:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-1" name="rest_code_766702da421f4028bece2717f3a3eb71-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;foo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt;
&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-2" name="rest_code_766702da421f4028bece2717f3a3eb71-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;foo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foo_bar&lt;/span&gt;
&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-3" name="rest_code_766702da421f4028bece2717f3a3eb71-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;foo&lt;/span&gt;
&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-4" name="rest_code_766702da421f4028bece2717f3a3eb71-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-5" name="rest_code_766702da421f4028bece2717f3a3eb71-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-5"&gt;&lt;/a&gt;&lt;span class="c1"&gt;# These all call the same function but look different in AST:&lt;/span&gt;
&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-6" name="rest_code_766702da421f4028bece2717f3a3eb71-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-6"&gt;&lt;/a&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-7" name="rest_code_766702da421f4028bece2717f3a3eb71-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-7"&gt;&lt;/a&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_766702da421f4028bece2717f3a3eb71-8" name="rest_code_766702da421f4028bece2717f3a3eb71-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_766702da421f4028bece2717f3a3eb71-8"&gt;&lt;/a&gt;&lt;span class="n"&gt;foo_bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is however, an advantage to this – you don’t need a real import to construct your bad examples, you can just use a Mock. e.g. for my &lt;code class="docutils literal"&gt;inclusion_tag&lt;/code&gt; example above, I have code like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-1" name="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_32639fb27ca1435ca4f23d63b2d39f2c-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mock&lt;/span&gt;
&lt;a id="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-2" name="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_32639fb27ca1435ca4f23d63b2d39f2c-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-3" name="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_32639fb27ca1435ca4f23d63b2d39f2c-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-4" name="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_32639fb27ca1435ca4f23d63b2d39f2c-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-5" name="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_32639fb27ca1435ca4f23d63b2d39f2c-5"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inclusion_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"something/not_bad_tag.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-6" name="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_32639fb27ca1435ca4f23d63b2d39f2c-6"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_tag&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;  &lt;span class="c1"&gt;# pyastgrep: expected&lt;/span&gt;
&lt;a id="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-7" name="rest_code_32639fb27ca1435ca4f23d63b2d39f2c-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_32639fb27ca1435ca4f23d63b2d39f2c-7"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can see the &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/fbc9609f6f5e10d8fd8ea15b0eb272d37869ed34/cciw/cciwmain/tests/test_conventions.py#L38"&gt;full code on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You might be able to use a mixture of techniques:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;A Semgrep rule avoids one set of bad patterns using some &lt;code class="docutils literal"&gt;thirdparty.func&lt;/code&gt;, and requiring everyone to use your own wrapper, which is then constructed in such a way to make it easier to apply a pyastgrep rule&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some introspection that produces a list of classes or functions to which some rule applies, then dynamically generates XPath expression to pass to pyastgrep.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Syntax level searching isn’t right for every job, but it can be a powerful addition to your toolkit, and with a decent query language like XPath, you can do a surprising amount. Have a look at the &lt;a class="reference external" href="https://pyastgrep.readthedocs.io/en/latest/examples.html"&gt;pyastgrep examples&lt;/a&gt; for inspiration!&lt;/p&gt;
&lt;/section&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/rp9rzq/pyastgrep_custom_linting"&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="django" label="Django"/>
    <category term="pyastgrep" label="pyastgrep"/>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
  </entry>
  <entry>
    <title>Programming mantras are proverbs</title>
    <id>https://lukeplant.me.uk/blog/posts/programming-mantras-are-proverbs/</id>
    <updated>2024-05-07T20:27:26+01:00</updated>
    <published>2024-05-07T20:27:26+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/programming-mantras-are-proverbs/"/>
    <summary type="html">&lt;p&gt;Proverbs are supposed to encapsulate a bit of wisdom, but you still need know when to apply it.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;I believe that many of the arguments we have around software development practices could be avoided by the simple understanding that all of our mantras need to be understood as &lt;strong&gt;proverbs&lt;/strong&gt; and not &lt;strong&gt;laws&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you understand proverbs, then you’ll know that &lt;a class="reference external" href="https://wadler.blogspot.com/2011/03/law-of-proverbs.html"&gt;every proverb has an equal and opposite proverb&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So, that means it’s fine to say DRY - &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;Don’t Repeat Yourself&lt;/a&gt;, and also WET - &lt;a class="reference external" href="https://www.franciscomoretti.com/code-tips/write-everything-twice-wet"&gt;Write Everything Twice&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It means it’s fine that we quote Knuth saying &lt;a class="reference external" href="https://en.wikiquote.org/wiki/Donald_Knuth"&gt;premature optimization is the root of all evil&lt;/a&gt;, while also believing that &lt;a class="reference external" href="https://blog.codinghorror.com/performance-is-a-feature/"&gt;Performance is a Feature&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We can understand and apply &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Separation_of_concerns"&gt;separation of concerns&lt;/a&gt;, while also seeing the value of &lt;a class="reference external" href="https://htmx.org/essays/locality-of-behaviour/"&gt;locality of behaviour&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is a time to &lt;a class="reference external" href="https://martinfowler.com/bliki/Yagni.html"&gt;YAGNI&lt;/a&gt;, and there is a time to &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/yagni-exceptions/"&gt;refrain from YAGNI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;None of the above involve logical contradictions. Proverbs encapsulate a small bit of wisdom in a pithy phrase, but you still need to learn to apply that wisdom correctly.&lt;/p&gt;
&lt;p&gt;Good proverbs are also supposed to make you &lt;strong&gt;think&lt;/strong&gt;. They slow you down before you leap into something that is likely to be bad, or they help you reflect on what did go wrong.&lt;/p&gt;
&lt;p&gt;One pair of Biblical proverbs that illustrate both of these things well is found in &lt;a class="reference external" href="https://www.biblegateway.com/passage/?search=Proverbs%2026%3A4-5&amp;amp;version=NIVUK"&gt;Proverbs 26:4-5&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;4 Do not answer a fool according to his folly,&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;or you yourself will be just like him.&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line"&gt;5 Answer a fool according to his folly,&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;or he will be wise in his own eyes.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first step is you are supposed to think: is it worth arguing in this situation?&lt;/p&gt;
&lt;p&gt;The second step, when you’ve had multiple failed interactions with a person, is to think: is it ever worth arguing with them? If the person is a fool, then you just can’t win.&lt;/p&gt;
&lt;p&gt;The third step is when you think like this: I notice that no-one ever argues with me for long. Is that because I’m always right, or because I’m a fool who refuses to lose an argument?&lt;/p&gt;
&lt;p&gt;So when it comes to programming, you’re not just supposed to pick whichever of the conflicting proverbs suits what you wanted to do anyway, you’re supposed to recognise you might be about to do something silly, and reflect before you continue.&lt;/p&gt;
&lt;p&gt;If it looks like you’re about to go against accumulated wisdom, you need to think about where the proverb came from, and the consequences of ignoring it.&lt;/p&gt;
&lt;p&gt;For example, with “DRY”, you need to understand that it’s not about repetitive or boring code, but about &lt;a class="reference external" href="https://verraes.net/2014/08/dry-is-about-knowledge/"&gt;embedding the same knowledge into a system in multiple places&lt;/a&gt;, and how this is a problem when you come to change something. WET, on the other hand, is about &lt;a class="reference external" href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction"&gt;premature abstractions being more costly than duplication&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once you’ve slowed down enough to think about it, you can work out which applies in your situation, and what you can do to mitigate the difficulties you might encounter if you choose to ignore the proverb – in other words, you can apply &lt;a class="reference external" href="https://en.wikipedia.org/wiki/G._K._Chesterton#Chesterton's_fence"&gt;Chesterton’s Fence&lt;/a&gt;.&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/ouybxe/programming_mantras_are_proverbs"&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="software-development" label="Software development"/>
  </entry>
</feed>
