<?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 Web development)</title>
  <id>https://lukeplant.me.uk/blog/categories/web-development.xml</id>
  <updated>2026-04-27T18:34:43Z</updated>
  <author>
    <name>Luke Plant</name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://lukeplant.me.uk/blog/categories/web-development.xml"/>
  <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/categories/web-development/"/>
  <generator uri="https://getnikola.com/">Nikola</generator>
  <entry>
    <title>Help my website is too small</title>
    <id>https://lukeplant.me.uk/blog/posts/help-my-website-is-too-small/</id>
    <updated>2025-12-19T13:45:33Z</updated>
    <published>2025-12-19T13:45:33Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/help-my-website-is-too-small/"/>
    <summary type="html">&lt;p&gt;How can it be a real website if it’s less than 7k?&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;A jobs web site I belong to just emailed me, telling me that some of the links in my public profile on their site are “broken” and “thus have been removed”.&lt;/p&gt;
&lt;p&gt;The evidence that these sites are broken? They are too small:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.djangoproject.com/"&gt;https://www.djangoproject.com/&lt;/a&gt;: response body too small (6220 bytes)&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.cciw.co.uk/"&gt;https://www.cciw.co.uk/&lt;/a&gt;: response body too small (3033 bytes)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first is the home page of the Django web framework, and is, unsurprisingly, implemented using Django (see the &lt;a class="reference external" href="https://github.com/django/djangoproject.com"&gt;djangoproject.com source code&lt;/a&gt;). The second is one of my own projects, and also implemented using Django (source &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/"&gt;also available&lt;/a&gt; for anyone who cares).&lt;/p&gt;
&lt;p&gt;Checking in webdev tools on these sites gives very similar numbers to the above for the over-the-wire size of the initial HTML (though I get slightly higher figures), so this wasn’t a blip caused by downtime, as far as I can see.&lt;/p&gt;
&lt;p&gt;Apparently, if your HTML is less than 7k, that obviously can’t be a real website, let alone something as ridiculously small as 3k. Even with compression turned up all the way, it’s clearly impossible to return more than an error message with less than &lt;a class="reference external" href="https://minime.stephan-brumme.com/react/18.0.0/"&gt;at least 4k&lt;/a&gt;, right?&lt;/p&gt;
&lt;p&gt;So please can Django get it sorted and add some bloat to their home page, and to their framework, and can someone also send me tips on bloating my own sites, so that my profile links can be counted as real websites? Thanks!&lt;/p&gt;
&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/3vdhci/help_my_website_is_too_small"&gt;Discussion of this post on Lobsters&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://news.ycombinator.com/item?id=46373559"&gt;Discussion of this post on Hacker News&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="web-development" label="Web 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>Enforcing conventions in Django projects with introspection</title>
    <id>https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/</id>
    <updated>2024-04-01T16:05:03+01:00</updated>
    <published>2024-04-01T16:05:03+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/"/>
    <summary type="html">&lt;p&gt;Some code and tips to combine Python and Django introspection APIs to enforce naming conventions in your Django models.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Naming conventions can make a big difference to maintenance issues in software
projects. This post is about how we can use the great introspection capabilities
in &lt;a class="reference external" href="https://www.python.org/"&gt;Python&lt;/a&gt; to help enforce naming conventions in
&lt;a class="reference external" href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt; projects.&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/enforcing-conventions-in-django-projects-with-introspection/#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/enforcing-conventions-in-django-projects-with-introspection/#the-problem-datefield-and-datetimefield-confusion" id="toc-entry-1"&gt;The problem: DateField and DateTimeField confusion&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/enforcing-conventions-in-django-projects-with-introspection/#the-tools" id="toc-entry-2"&gt;The tools&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/enforcing-conventions-in-django-projects-with-introspection/#introspection" id="toc-entry-3"&gt;Introspection&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/enforcing-conventions-in-django-projects-with-introspection/#django-app-and-model-introspection" id="toc-entry-4"&gt;Django app and model introspection&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/enforcing-conventions-in-django-projects-with-introspection/#django-checks-framework" id="toc-entry-5"&gt;Django checks framework&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/enforcing-conventions-in-django-projects-with-introspection/#the-solution" id="toc-entry-6"&gt;The solution&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/enforcing-conventions-in-django-projects-with-introspection/#output" id="toc-entry-7"&gt;Output&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/enforcing-conventions-in-django-projects-with-introspection/#conclusion" id="toc-entry-8"&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/enforcing-conventions-in-django-projects-with-introspection/#links" id="toc-entry-9"&gt;Links&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;p&gt;Let’s start with an example problem and the naming convention we’re going to use to solve it. There are many other applications of the techniques here, but it helps to have something concrete.&lt;/p&gt;
&lt;section id="the-problem-datefield-and-datetimefield-confusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-1" role="doc-backlink"&gt;The problem: DateField and DateTimeField confusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Over several projects I’ve found that inconsistent or bad naming of &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.DateField"&gt;DateField&lt;/a&gt; and &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.DateTimeField"&gt;DateTimeField&lt;/a&gt; fields can cause various problems.&lt;/p&gt;
&lt;p&gt;First, poor naming means that you can confuse them for each other, and this can easily trip you up. In Python, &lt;a class="reference external" href="https://docs.python.org/3/library/datetime.html#datetime.datetime"&gt;datetime&lt;/a&gt; is a subclass of &lt;a class="reference external" href="https://docs.python.org/3/library/datetime.html#datetime.date"&gt;date&lt;/a&gt;, so if you use a field called &lt;code class="docutils literal"&gt;created_date&lt;/code&gt; assuming it holds a &lt;code class="docutils literal"&gt;date&lt;/code&gt; when it actually holds a &lt;code class="docutils literal"&gt;datetime&lt;/code&gt;, it might be not obvious initially that you are mishandling the value, but you’ll often have subtle problems down the line.&lt;/p&gt;
&lt;p&gt;Second, sometimes you have a field named like &lt;code class="docutils literal"&gt;expired&lt;/code&gt; which is actually the timestamp of when the record expired, but it could easily be confused for a boolean field.&lt;/p&gt;
&lt;p&gt;Third, not having a strong convention, or having multiple conventions, leads to unnecessary time wasted on decisions that could have been made once.&lt;/p&gt;
&lt;p&gt;Finally, inconsistency in naming is just confusing and ugly for developers, and often for users further down the line, because names tend to leak.&lt;/p&gt;
&lt;p&gt;Even if you do have an established convention, it’s possible for people not to know. It’s also very easy for people to change a field’s type between &lt;code class="docutils literal"&gt;date&lt;/code&gt; and &lt;code class="docutils literal"&gt;datetime&lt;/code&gt; without also changing the name. So merely having the convention is not enough, it needs to be enforced.&lt;/p&gt;
&lt;aside class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;If you want to change the name &lt;strong&gt;and&lt;/strong&gt; type of a field (or any other atribute), and want to preserve data as much as possible, you usually need to do it in two stages or more depending on your needs – otherwise Django’s migration framework will just see one field removed and a completely different one added, and generate migrations that will destroy your data. Always check the migrations created.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;For this specific example, the convention I quite like is:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;field names should end with &lt;code class="docutils literal"&gt;_at&lt;/code&gt; for timestamp fields that use &lt;code class="docutils literal"&gt;DateTimeField&lt;/code&gt;, like &lt;code class="docutils literal"&gt;expires_at&lt;/code&gt; or &lt;code class="docutils literal"&gt;deleted_at&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;field names should end with &lt;code class="docutils literal"&gt;_on&lt;/code&gt; or &lt;code class="docutils literal"&gt;_date&lt;/code&gt; for fields that use &lt;code class="docutils literal"&gt;DateField&lt;/code&gt;, like &lt;code class="docutils literal"&gt;issued_on&lt;/code&gt; or &lt;code class="docutils literal"&gt;birth_date&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is based on the English grammar rule that we use “on” for dates but “at” for times –  “on the 25th March”, but “at 7:00 pm” – and conveniently it also  needs very few letters and tends to read well in code. The &lt;code class="docutils literal"&gt;_date&lt;/code&gt; suffix is also helpful in various contexts where &lt;code class="docutils literal"&gt;_on&lt;/code&gt; seems very unnatural. You might want different conventions, of course.&lt;/p&gt;
&lt;p&gt;To get our convention to be enforced with automated checks we need a few tools.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="the-tools"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-2" role="doc-backlink"&gt;The tools&lt;/a&gt;&lt;/h2&gt;
&lt;section id="introspection"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-3" role="doc-backlink"&gt;Introspection&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Introspection&lt;/strong&gt; means the ability to use code to inspect code, and typically we’re talking about doing this when our code is already running, from within the same program and using the same programming language.&lt;/p&gt;
&lt;p&gt;In Python, this starts from simple things like &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#isinstance"&gt;isinstance()&lt;/a&gt; and &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#type"&gt;type()&lt;/a&gt; to check the type of
an object, to things like &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#hasattr"&gt;hasattr()&lt;/a&gt; to check for the
presence of attributes and many other more advanced techniques, including the &lt;a class="reference external" href="https://docs.python.org/3/library/inspect.html"&gt;inspect&lt;/a&gt; module and many of the &lt;a class="reference external" href="https://www.pythonmorsels.com/every-dunder-method/#metaprogramming"&gt;metaprogramming dunder methods&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="django-app-and-model-introspection"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-4" role="doc-backlink"&gt;Django app and model introspection&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Django is just Python, so you can use all normal Python introspection techniques. In addition, there is a formally documented and supported set of functions and methods for introspecting Django apps and models, such as the &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/applications/"&gt;apps module&lt;/a&gt; and the &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/models/meta/"&gt;Model _meta API&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="django-checks-framework"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-5" role="doc-backlink"&gt;Django checks framework&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The third main tool we’re going to use in this solution is Django’s &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/topics/checks/"&gt;system checks framework&lt;/a&gt;, which allows us to run certain kinds of checks, at both “warning” and “error” level. This is the least important tool, and we could in fact switch it out for something else like a unit test.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="the-solution"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-6" role="doc-backlink"&gt;The solution&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It’s easiest to present the code, and then discuss it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-1" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-1" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.apps&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;apps&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-2" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-2" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.conf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-3" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-3" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.core.checks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ne"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-4" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-4" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-5" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-5" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-6" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-6" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-6"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-7" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-7" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-7"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_date_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_configs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-8" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-8" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-8"&gt;&lt;/a&gt;    &lt;span class="n"&gt;exceptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-9" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-9" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-9"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# This field is provided by Django's AbstractBaseUser, we don't control it&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-10" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-10" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-10"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# and we’ll break things if we change it:&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-11" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-11" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-11"&gt;&lt;/a&gt;        &lt;span class="s2"&gt;"accounts.User.last_login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-12" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-12" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-12"&gt;&lt;/a&gt;    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-13" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-13" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-13"&gt;&lt;/a&gt;    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.db.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DateField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeField&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-14" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-14" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-14"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-15" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-15" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-15"&gt;&lt;/a&gt;    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-16" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-16" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-16"&gt;&lt;/a&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;get_first_party_fields&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-17" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-17" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-17"&gt;&lt;/a&gt;        &lt;span class="n"&gt;field_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-18" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-18" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-18"&gt;&lt;/a&gt;        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-19" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-19" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-19"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-20" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-20" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-20"&gt;&lt;/a&gt;        &lt;span class="k"&gt;if&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;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app_label&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;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-21" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-21" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-21"&gt;&lt;/a&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-22" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-22" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-22"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-23" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-23" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-23"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# Order of checks here is important, because DateTimeField inherits from DateField&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-24" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-24" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-24"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-25" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-25" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-25"&gt;&lt;/a&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;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeField&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-26" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-26" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-26"&gt;&lt;/a&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"_at"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-27" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-27" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-27"&gt;&lt;/a&gt;                &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-28" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-28" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-28"&gt;&lt;/a&gt;                    &lt;span class="ne"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-29" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-29" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-29"&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;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; field expected to end with `_at`, "&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-30" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-30" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-30"&gt;&lt;/a&gt;                        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"or be added to the exceptions in this check."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-31" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-31" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-31"&gt;&lt;/a&gt;                        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-32" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-32" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-32"&gt;&lt;/a&gt;                        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"conventions.E001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-33" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-33" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-33"&gt;&lt;/a&gt;                    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-34" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-34" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-34"&gt;&lt;/a&gt;                &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-35" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-35" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-35"&gt;&lt;/a&gt;        &lt;span class="k"&gt;elif&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;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateField&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-36" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-36" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-36"&gt;&lt;/a&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"_date"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"_on"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-37" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-37" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-37"&gt;&lt;/a&gt;                &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-38" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-38" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-38"&gt;&lt;/a&gt;                    &lt;span class="ne"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-39" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-39" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-39"&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;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; field expected to end with `_date` or `_on`, "&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-40" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-40" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-40"&gt;&lt;/a&gt;                        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"or be added to the exceptions in this check."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-41" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-41" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-41"&gt;&lt;/a&gt;                        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-42" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-42" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-42"&gt;&lt;/a&gt;                        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"conventions.E002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-43" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-43" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-43"&gt;&lt;/a&gt;                    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-44" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-44" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-44"&gt;&lt;/a&gt;                &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-45" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-45" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-45"&gt;&lt;/a&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-46" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-46" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-46"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-47" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-47" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-47"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-48" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-48" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-48"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_first_party_fields&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-49" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-49" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-49"&gt;&lt;/a&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;app_config&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;get_first_party_apps&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-50" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-50" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-50"&gt;&lt;/a&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;app_config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_models&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-51" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-51" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-51"&gt;&lt;/a&gt;            &lt;span class="k"&gt;yield from&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_fields&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-52" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-52" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-52"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-53" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-53" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-53"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-54" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-54" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-54"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_first_party_apps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-55" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-55" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-55"&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;app_config&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;app_config&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;apps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_app_configs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_first_party_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_config&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-56" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-56" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-56"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-57" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-57" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-57"&gt;&lt;/a&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-58" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-58" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-58"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_first_party_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AppConfig&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-59" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-59" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-59"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;app_config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIRST_PARTY_APPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-60" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-60" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-60"&gt;&lt;/a&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-61" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-61" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-61"&gt;&lt;/a&gt;    &lt;span class="n"&gt;app_config_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app_config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__class__&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-62" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-62" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-62"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&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;app_config_class&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__module__&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;app_config_class&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIRST_PARTY_APPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-63" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-63" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-63"&gt;&lt;/a&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;a id="rest_code_9951de097b2c4cc5a5c51df1006828fc-64" name="rest_code_9951de097b2c4cc5a5c51df1006828fc-64" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_9951de097b2c4cc5a5c51df1006828fc-64"&gt;&lt;/a&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We start here with some imports and registration, as documented in the “System checks” docs. You’ll need to place this code &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/topics/checks/#registering-and-labeling-checks"&gt;somewhere that will be loaded when your application is loaded&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Our checking function defines some allowed exceptions, because there are some things out of our control, or there might be other reasons. It also mentions the exceptions mechanism in the warning message. You might want a different mechanism here, but I think having some way of dealing with exceptions, and advertising its existence in the warnings, is often pretty important. Otherwise, you can end up with worse consequences when people just slavishly follow rules. Notice how in the exception list above I’ve given a comment detailing &lt;strong&gt;why&lt;/strong&gt; the exception is there though – this helps to establish a precedent that exceptions should be &lt;strong&gt;justified&lt;/strong&gt;, and the justification should be there in the code.&lt;/p&gt;
&lt;p&gt;We then loop through all “first party” model fields, looking for &lt;code class="docutils literal"&gt;DateTimeField&lt;/code&gt; and &lt;code class="docutils literal"&gt;DateField&lt;/code&gt; instances. This is done using our &lt;code class="docutils literal"&gt;get_first_party_fields()&lt;/code&gt; utility, which is defined in terms of &lt;code class="docutils literal"&gt;get_first_party_apps()&lt;/code&gt;, which in turn depends on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/applications/#django.apps.apps.get_app_configs"&gt;get_app_configs() function&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/applications/#django.apps.AppConfig.get_models"&gt;AppConfig.get_models() method&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the &lt;a class="reference external" href="https://docs.djangoproject.com/en/5.0/ref/models/meta/#django.db.models.options.Options.get_fields"&gt;_meta get_fields() method&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a custom setting &lt;code class="docutils literal"&gt;FIRST_PARTY_APPS&lt;/code&gt; which I’ve created in my &lt;code class="docutils literal"&gt;settings.py&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-1" name="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-1" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_4153e5400ccc47ef917c89b1b83e9dd6-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;FIRST_PARTY_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"myotherapp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-2" name="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-2" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_4153e5400ccc47ef917c89b1b83e9dd6-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-3" name="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-3" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_4153e5400ccc47ef917c89b1b83e9dd6-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;a id="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-4" name="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-4" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_4153e5400ccc47ef917c89b1b83e9dd6-4"&gt;&lt;/a&gt; &lt;span class="s2"&gt;"django.contrib.auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-5" name="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-5" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_4153e5400ccc47ef917c89b1b83e9dd6-5"&gt;&lt;/a&gt; &lt;span class="s2"&gt;"django.contrib.sessions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-6" name="rest_code_4153e5400ccc47ef917c89b1b83e9dd6-6" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#rest_code_4153e5400ccc47ef917c89b1b83e9dd6-6"&gt;&lt;/a&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FIRSTY_PARTY_APPS&lt;/span&gt; &lt;span class="o"&gt;+&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;/pre&gt;&lt;/div&gt;
&lt;p&gt;You may have a different way of recognising your own apps.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code class="docutils literal"&gt;id&lt;/code&gt; values passed to &lt;code class="docutils literal"&gt;Warning&lt;/code&gt; here are examples – you should change according to your needs. You might also choose to use &lt;code class="docutils literal"&gt;Error&lt;/code&gt; instead of &lt;code class="docutils literal"&gt;Warning&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="output"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-7" role="doc-backlink"&gt;Output&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When you run &lt;code class="docutils literal"&gt;manage.py check&lt;/code&gt;, you’ll then get output like:&lt;/p&gt;
&lt;div style="color: #b2b2b2; background-color: #292b2e;"&gt;
&lt;pre&gt;
 System check identified some issues:

 &lt;span style="color: #bc6ec5; font-weight: bold;"&gt;WARNINGS&lt;/span&gt;:
 &lt;span style="color: #b1951d; font-weight: bold;"&gt;myapp.MyModel.created&lt;/span&gt;&lt;span style="color: #b1951d;"&gt;: (conventions.E001) MyModel.created field expected to end with `_at`,
 or be added to the exceptions in this check.&lt;/span&gt;

 System check identified 1 issue (0 silenced).
&lt;/pre&gt;
&lt;/div&gt;&lt;p&gt;As mentioned, you might instead want to run this kind of check as a unit test.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-8" role="doc-backlink"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are many variations on this technique that can be used to great effect in Django or other Python projects. Very often you will be able to &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/"&gt;play around with a REPL&lt;/a&gt; to do the introspection you need.&lt;/p&gt;
&lt;p&gt;Where it is possible, I find doing this far more effective than attempting to document things and relying on people reading and remembering those docs. Every time I’m tripped up by bad names, or when good names or a strong convention could have helped me, I try to think about how I could push people towards a good convention automatically – while also giving a thought to unintended bad consequences of doing that prematurely or too forcefully.&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/enforcing-conventions-in-django-projects-with-introspection/#toc-entry-9" role="doc-backlink"&gt;Links&lt;/a&gt;&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;For other ideas and techniques for this kind of thing, see Haki Benita’s &lt;a class="reference external" href="https://hakibenita.com/automating-the-boring-stuff-in-django-using-the-check-framework"&gt;Automating the Boring Stuff in Django Using the Check Framework&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://lobste.rs/s/4nnfdb/enforcing_conventions_django_projects"&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="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>Super-fast Sphinx docs, and SNOB driven development</title>
    <id>https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/</id>
    <updated>2023-09-27T15:05:48+01:00</updated>
    <published>2023-09-27T15:05:48+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/"/>
    <summary type="html">&lt;p&gt;Code that will make your static doc pages seriously faster, that you seriously don’t need&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;If you are using static HTML files for your docs, such as with &lt;a class="reference external" href="https://www.sphinx-doc.org/en/master/"&gt;Sphinx&lt;/a&gt; or many other doc generators, here is a chunk of code that will speed up loading of pages after the first one. If you’re using some other docs generator, the instructions will probably work with minimal adaptation.&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p&gt;Create a &lt;code class="docutils literal"&gt;custom.js&lt;/code&gt; file inside your &lt;code class="docutils literal"&gt;_static&lt;/code&gt; directory, with the following contents:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code javascript"&gt;&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-1" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-1" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-1"&gt;&lt;/a&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'script'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-2" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-2" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-2"&gt;&lt;/a&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://unpkg.com/htmx.org@1.9.5"&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-3" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-3" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-3"&gt;&lt;/a&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;integrity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha384-xcuj3WpfgjlKF+FXhSQFQ0ZNr39ln+hwjN3npfM9VBnUskLolQAcN80McRIVOPuO"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-4" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-4" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-4"&gt;&lt;/a&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'anonymous'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-5" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-5" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-5"&gt;&lt;/a&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&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_31bcb3a456dd410fa604970cf8ee40ea-6" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-6" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-6"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-7" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-7" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-7"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hx-boost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-8" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-8" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-9" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-9" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-9"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_31bcb3a456dd410fa604970cf8ee40ea-10" name="rest_code_31bcb3a456dd410fa604970cf8ee40ea-10" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_31bcb3a456dd410fa604970cf8ee40ea-10"&gt;&lt;/a&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&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;Add an item to your &lt;code class="docutils literal"&gt;html_js_files&lt;/code&gt; setting in your Sphinx &lt;code class="docutils literal"&gt;conf.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_c1d3d697755946c394928e2e0453d51c-1" name="rest_code_c1d3d697755946c394928e2e0453d51c-1" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_c1d3d697755946c394928e2e0453d51c-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;html_js_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;a id="rest_code_c1d3d697755946c394928e2e0453d51c-2" name="rest_code_c1d3d697755946c394928e2e0453d51c-2" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_c1d3d697755946c394928e2e0453d51c-2"&gt;&lt;/a&gt;    &lt;span class="s1"&gt;'custom.js'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_c1d3d697755946c394928e2e0453d51c-3" name="rest_code_c1d3d697755946c394928e2e0453d51c-3" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_c1d3d697755946c394928e2e0453d51c-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Rebuild and you’re done.&lt;/p&gt;
&lt;p&gt;What this script does is:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;p&gt;Load the &lt;a class="reference external" href="https://htmx.org/"&gt;htmx&lt;/a&gt; library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If it successfully loads, adds the &lt;a class="reference external" href="https://htmx.org/attributes/hx-boost/"&gt;hx-boost&lt;/a&gt; attribute to the body element.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Initialises htmx on the page.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means that htmx will intercept all internal links on the page, and instead of letting the browser load them the normal way, it sends an AJAX request and swaps in the content of the page. This means that the whole page doesn’t need to be reloaded by the browser, saving precious milliseconds.&lt;/p&gt;
&lt;section id="actually-please-dont"&gt;
&lt;h2&gt;Actually, please don’t&lt;/h2&gt;
&lt;p&gt;I will provide reasons why you really shouldn’t use the code above, although it works almost perfectly. But first, a rant.&lt;/p&gt;
&lt;p&gt;This post was inspired by &lt;a class="reference external" href="https://www.mux.com/blog/what-are-react-server-components"&gt;Mux’s blog post on migrating 50,000 lines of React Server Components&lt;/a&gt;. It contains a nice overview of the history of web site architecture, including this quote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Then, we started wondering: What if we wanted faster responses and more interactivity? Every time a user takes an action, do we really want to send cookies back to the server and make the server generate a whole new page? What if we made the client do that work instead? We can just send all the rendering code to the client as JavaScript!&lt;/p&gt;
&lt;p&gt;This was called client-side rendering (CSR) or single-page applications (SPA) and was &lt;a class="reference external" href="https://begin.com/blog/posts/2023-02-21-why-does-everyone-suddenly-hate-single-page-apps"&gt;widely considered a bad move&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, instead of then suggesting that we perhaps we should retrace our steps, the article just plunges on and on, deeper and deeper into the jungle.&lt;/p&gt;
&lt;p&gt;Now, this might all make sense if we are talking about a highly interactive site that has the highest possible needs in terms of user interactivity. But I realised the article was about &lt;strong&gt;just their documentation site&lt;/strong&gt;, not the main application.&lt;/p&gt;
&lt;p&gt;Now, some docs sites are really fancy and do very clever interactive things. Mux’s, however, is not like that. The only interactive things I could find were:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;tabs – like you can get with something like &lt;a class="reference external" href="https://sphinx-code-tabs.readthedocs.io/en/latest/"&gt;sphinx-code-tabs&lt;/a&gt;, powered by a &lt;a class="reference external" href="https://sphinx-code-tabs.readthedocs.io/en/latest/_static/code-tabs.js"&gt;tiny bit of Javascript&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;their &lt;a class="reference external" href="https://docs.mux.com/changelog"&gt;changelog page&lt;/a&gt; – which is more complicated, but whose essential functionality could again be implemented by a really small amount of Javascript added to a static page. I should also note that their page is really pretty slugish when you change the filters, much slower than you would get by an approach that just selectively hides parts of the page using DOM manipulation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;search. Search is definitely important, but I can’t see why it means the whole site needs to be implemented in React.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A “Was this helpful” component – this could have been a small web component or something similar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A few fancy transitions in the side bar.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are not the highly stateful pages that React was designed for. Maybe there are a few other things I didn’t find, but 95% of it could be handled using entirely static HTML, built by any number of simple docs generators, with tiny amounts of Javascript.&lt;/p&gt;
&lt;p&gt;The only other thing I noticed is that page transitions generally had that instant feel an SPA can give you, and were noticeably faster than you would get with the static HTML solution I’m suggesting.&lt;/p&gt;
&lt;p&gt;So, not to be beaten, I came up with the above solution on htmx so I could match the speed.&lt;/p&gt;
&lt;p&gt;Now, here’s why you shouldn’t use it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A typical docs page with Sphinx loads in a few hundred milliseconds, which is fine. Do you really need to shave that down to less than 50 so it feels “instant”? Do your users care?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;While it is truly a tiny fraction of the complexity of the React docs site Mux described in their post, you are still adding some significant complexity. Is it worth is?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are you sure it’s not going to interact badly with some Javascript on some page, maybe some future Javascript you will add?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have you considered all use cases – like the person who downloads your whole docs site using &lt;code class="docutils literal"&gt;wget &lt;span class="pre"&gt;--recursive&lt;/span&gt;&lt;/code&gt; so they can browse offline? Answer: if they have no internet connection when they view the docs, it will actually work fine, because the htmx library won’t load at all. But if they are online, the htmx library will load, and then every internal link will break due to &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"&gt;CORS errors&lt;/a&gt;. &lt;strong&gt;You just broke offline viewing&lt;/strong&gt;. You could fix this very easily with an extra conditional in the script above, but I’m making a point. Is there anything else that’s broken?&lt;/p&gt;
&lt;p&gt;No prizes for guessing that while Sphinx-generated sites normally work perfectly with &lt;code class="docutils literal"&gt;wget &lt;span class="pre"&gt;--recursive&lt;/span&gt;&lt;/code&gt; for offline viewing, docs.mux.com does not work well, to put it mildly. I also wasted hundreds of Mb finding out, due to the vast amount of boilerplate every single HTML file has. Don’t be like them.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is what you should actually do:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;recognise that you know exactly how to make your documentation pages load instantly, like an SPA, and could absolutely do it if you wanted to, still with a tiny fraction of the complexity of an actual SPA architecture, and with fixes for the issues I’ve mentioned, in about 15 minutes, then,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;don’t.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As protection against the FOMO and fashion that drives so much of web development, this attitude needs a catchy slogan, which is the kind of thing I’m not very good at. But as a first attempt, how about: SNOB driven development. SNOB means “Smugly kNOwing Better”. Or maybe that could be “Smugly NO-ing Better”.&lt;/p&gt;
&lt;p&gt;Join me. Be an arrogant SNOB and just say No.&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/kjleh7/super_fast_sphinx_docs_snob_driven"&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="rants" label="Rants"/>
    <category term="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>No one actually wants simplicity</title>
    <id>https://lukeplant.me.uk/blog/posts/no-one-actually-wants-simplicity/</id>
    <updated>2023-08-22T18:49:31+01:00</updated>
    <published>2023-08-22T18:49:31+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/no-one-actually-wants-simplicity/"/>
    <summary type="html">&lt;p&gt;We think we do, but in fact every web developer will happily sacrifice simplicity to the first shiny thing promising them relief from the mildest of ailments.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;The reason that modern web development is &lt;a class="reference external" href="https://www.youtube.com/watch?v=BtJAsvJOlhM"&gt;swamped with complexity&lt;/a&gt; is that no one really wants things to be simple. We just think we do, while our choices prove otherwise.&lt;/p&gt;
&lt;p&gt;A lot of developers want simplicity in the same way that a lot of clients claim they want a fast website. You respond “OK, so we can remove some of these 17 Javascript trackers and other bloat that’s making your website horribly slow?” – no, apparently those are all critical business functionality.&lt;/p&gt;
&lt;p&gt;In other words, they prioritise everything over speed. And then they wonder why using their website is like rowing a boat through a lake of molasses on a cold day using nothing but a small plastic spoon.&lt;/p&gt;
&lt;p&gt;The same is often true of complexity. The real test is the question “what are you willing to sacrifice to achieve simplicity?” If the answer is “nothing”, then you don’t actually love simplicity at all, it’s your lowest priority.&lt;/p&gt;
&lt;p&gt;When I say “sacrifice”, I don’t mean that choosing simplicity will mean you are worse off overall – simplicity brings massive benefits. But it does mean that there will be some things that tempt you to believe you are missing out.&lt;/p&gt;
&lt;p&gt;For every developer, it might be something different. For one, the tedium of having to spend half an hour a month ensuring that two different things are kept in sync easily justifies the adoption of a bulky framework that solves that particular problem. For another, the ability to control how a checkbox animates when you check it is of course a valid reason to add another 50 packages and 3 layers of frameworks to their product. For another, adding an abstraction with thousands of lines of codes, dozens of classes and page after page of documentation in order to avoid manually writing a &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/"&gt;tiny factory function for a test&lt;/a&gt; is a great trade-off.&lt;/p&gt;
&lt;p&gt;Of course we all claim to hate complexity, but it’s actually just complexity added by &lt;strong&gt;other&lt;/strong&gt; people that we hate — our own bugbears are always exempted, and for things we understand we quickly become unable to even see there is a potential problem for other people. Certainly there are frameworks and dependencies that justify their existence and adoption, but working out which ones they are is hard.&lt;/p&gt;
&lt;p&gt;I think a good test of whether you truly love simplicity is whether you are able to remove things you have added, especially code you’ve written, even when it is still providing &lt;strong&gt;some&lt;/strong&gt; value, because you realise it is not providing &lt;strong&gt;enough&lt;/strong&gt; value.&lt;/p&gt;
&lt;p&gt;Another test is what you are tempted to do when a problem arises with some of the complexity you’ve added. Is your first instinct to add even more stuff to fix it, or is it to remove and live with the loss?&lt;/p&gt;
&lt;p&gt;The only path I can see through all this is to cultivate an almost obsessive suspicion of &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Fear_of_missing_out"&gt;FOMO&lt;/a&gt;. I think that’s probably key to learning to &lt;a class="reference external" href="https://grugbrain.dev/#grug-on-saying-no"&gt;say no&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/ao2x0v/no_one_actually_wants_simplicity"&gt;Discussion of this post on Lobsters&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/no-one-actually-wants-simplicity/Nooneactuallywantssimplicity"&gt;Discussion of this post on Hacker News&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</content>
    <category term="rants" label="Rants"/>
    <category term="software-development" label="Software development"/>
    <category term="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>You can stop using user-scalable=no and maximum-scale=1 in viewport meta tags now</title>
    <id>https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/</id>
    <updated>2023-06-10T15:18:08+01:00</updated>
    <published>2023-06-10T15:18:08+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/"/>
    <summary type="html">&lt;p&gt;It’s bad for accessibility, and no longer needed.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Many websites are still using a &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag"&gt;viewport meta tag&lt;/a&gt; like one of the following:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code html"&gt;&lt;a id="rest_code_e9f2caf10bc44b369d321689a7b3a77d-1" name="rest_code_e9f2caf10bc44b369d321689a7b3a77d-1" href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/#rest_code_e9f2caf10bc44b369d321689a7b3a77d-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meta&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1, maximum-scale=1"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="code"&gt;&lt;pre class="code html"&gt;&lt;a id="rest_code_bd843a258df548c4bfeb7a6df5f1d40d-1" name="rest_code_bd843a258df548c4bfeb7a6df5f1d40d-1" href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/#rest_code_bd843a258df548c4bfeb7a6df5f1d40d-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meta&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1, user-scalable=no"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These days, you can almost certainly remove the &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;maximum-scale&lt;/span&gt;&lt;/code&gt; or &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;user-scalable&lt;/span&gt;&lt;/code&gt; properties, to leave:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code html"&gt;&lt;a id="rest_code_7e35992064f645e3997879a12b75088e-1" name="rest_code_7e35992064f645e3997879a12b75088e-1" href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/#rest_code_7e35992064f645e3997879a12b75088e-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meta&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is the same as suggested by &lt;a class="reference external" href="https://github.com/h5bp/html5-boilerplate/blob/main/src/index.html"&gt;HTML5 boilerplate&lt;/a&gt;, so it should be a pretty good default for most people.&lt;/p&gt;
&lt;p&gt;Why should you remove these properties? Because they’re bad for accessibility — they stop users on many mobile devices (mostly Android) from being able to zoom in and view things that would be too small otherwise. This doesn’t just affect people with impaired vision — as a fully sighted person I often find web pages where there are graphics with text and other details that are too small when using a mobile phone, and then I find I can’t zoom in either.&lt;/p&gt;
&lt;p&gt;Who says so? The A11Y Project says &lt;a class="reference external" href="https://www.a11yproject.com/posts/never-use-maximum-scale/"&gt;“Never use maximum-scale=1”&lt;/a&gt;, and &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag"&gt;MDN also agree&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;maximum-scale&lt;/span&gt;&lt;/code&gt;: Any value less than 3 fails accessibility&lt;/p&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;user-scalable&lt;/span&gt;&lt;/code&gt;: Setting the value to &lt;code class="docutils literal"&gt;0&lt;/code&gt;, which is the same as &lt;code class="docutils literal"&gt;no&lt;/code&gt;, is against Web Content Accessibility Guidelines (WCAG).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The final question, then, is “Can you?”.&lt;/p&gt;
&lt;p&gt;If you are like me you don’t want to remove something that was clearly added for some reason, which is a good instinct — see &lt;a class="reference external" href="https://wiki.lesswrong.com/wiki/Chesterton%27s_Fence"&gt;Chesterton’s fence&lt;/a&gt;. As far as I can tell, the practice of adding &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;user-scalable=no&lt;/span&gt;&lt;/code&gt; or &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;maximum-scale=1&lt;/span&gt;&lt;/code&gt; became widespread because of several browser bugs which are now irrelevant or best addressed with other fixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Using CSS &lt;code class="docutils literal"&gt;position:fixed&lt;/code&gt; only works in Android 2.1 thru 2.3 by using the
following meta tag: &amp;lt;meta name="viewport" content="width=device-width,
user-scalable=no"&amp;gt; (from &lt;a class="reference external" href="https://caniuse.com/css-fixed"&gt;caniuse.com&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;This should not be relevant to most users these days.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Safari on iOS, &lt;a class="reference external" href="https://stackoverflow.com/questions/11165460/responsive-site-is-zoomed-in-when-flipping-between-portrait-and-landscape-on-ipa"&gt;at least in the past&lt;/a&gt;, would “zoom in” when flipping from portrait to landscape, unless you added &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;maximum-scale=1&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;From what I can tell, this bug is probably fixed, and &lt;a class="reference external" href="https://css-tricks.com/probably-use-initial-scale1/"&gt;you get good behaviour when adding just initial-scale=1&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Safari on iOS has unhelpful zooming behaviour when you click on a text box and the keyboard pops up, which some people fix using &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;maximum-scale=1&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rick Strahl has &lt;a class="reference external" href="https://weblog.west-wind.com/posts/2023/Apr/17/Preventing-iOS-Safari-Textbox-Zooming"&gt;a comprehensive post on better fixes to this&lt;/a&gt;, which are basically:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;selectively add maximum-scale=1 to the viewport tag,  only on iOS Safari, using a small bit of Javascript. This works without breaking accessibility, because iOS Safari apparently ignores maximum-scale=1 when it comes to user-initiated zooming&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;setting &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;font-size:&lt;/span&gt; 16px&lt;/code&gt; or higher for form inputs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are a couple of final cases to address:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Some people want pages to behave more like native apps, where zooming wouldn’t even be possible. Before you do this, consider that you are making problems for many people across your site for the sake of your own aesthetic preference. And, it doesn’t work for recent iOS anyway because it deliberately ignores the properties for the sake of accessibility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You may need to control how zooming gestures work for certain components on the page. I believe the correct solution in this case is &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action"&gt;touch-action&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s all, thanks!&lt;/p&gt;</content>
    <category term="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>Re-using CSS for the wrong HTML with Sass</title>
    <id>https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/</id>
    <updated>2023-06-01T20:44:15Z</updated>
    <published>2023-06-01T20:44:15Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/"/>
    <summary type="html">&lt;p&gt;A trick I learned for using someone else’s CSS without changing your HTML, or their CSS&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Recently, while writing up &lt;a class="reference external" href="https://github.com/spookylukey/django-htmx-patterns/blob/master/form_validation.rst"&gt;some examples and pattern for using htmx with Django for form validation&lt;/a&gt;, I discovered a new trick for using externally defined CSS without having to change the HTML you are working with.&lt;/p&gt;
&lt;p&gt;To make it concrete, an example might be that you are using some CSS from a CSS library or framework that requires your HTML to look a certain way. In the &lt;a class="reference external" href="https://bulma.io/"&gt;Bulma&lt;/a&gt; framework, for instance, you have to add the right &lt;code class="docutils literal"&gt;class&lt;/code&gt; attribute directly on an element that needs styling.&lt;/p&gt;
&lt;p&gt;At the same time, you might be working with another system that is generating the HTML for you, and modifying that output might be hard or impossible or just tedious and a potential maintenance burden going forward. For instance, in &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/forms/api/"&gt;Django forms&lt;/a&gt;, there is an &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/forms/api/#customizing-the-error-list-format"&gt;ErrorList class&lt;/a&gt; whose output can be overridden, but by default renders like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code html"&gt;&lt;a id="rest_code_c03cf717e89045f99f7bb52a4c3f9527-1" name="rest_code_c03cf717e89045f99f7bb52a4c3f9527-1" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_c03cf717e89045f99f7bb52a4c3f9527-1"&gt;&lt;/a&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"errorlist"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c03cf717e89045f99f7bb52a4c3f9527-2" name="rest_code_c03cf717e89045f99f7bb52a4c3f9527-2" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_c03cf717e89045f99f7bb52a4c3f9527-2"&gt;&lt;/a&gt;  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Enter a valid email address.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_c03cf717e89045f99f7bb52a4c3f9527-3" name="rest_code_c03cf717e89045f99f7bb52a4c3f9527-3" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_c03cf717e89045f99f7bb52a4c3f9527-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now I have these requirements:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;I want this error list to be coloured using a Bulma &lt;a class="reference external" href="https://bulma.io/documentation/helpers/color-helpers/#text-color"&gt;colour utility&lt;/a&gt; as if it had &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;class="has-text-danger"&lt;/span&gt;&lt;/code&gt; when it appears within a field row (which are &lt;code class="docutils literal"&gt;&amp;lt;div &lt;span class="pre"&gt;class="field"&amp;gt;&lt;/span&gt;&lt;/code&gt; elements).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When it appears at the top of the form where it has an extra &lt;code class="docutils literal"&gt;nofield&lt;/code&gt; class, I want it to instead be styled like a Bulma &lt;a class="reference external" href="https://bulma.io/documentation/elements/notification/"&gt;notification&lt;/a&gt; as if it had &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;class="notification&lt;/span&gt; &lt;span class="pre"&gt;is-danger&lt;/span&gt; &lt;span class="pre"&gt;is-light"&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But I want to do these without changing the HTML we’re given by Django, or changing existing CSS – only by adding some CSS rules.&lt;/p&gt;
&lt;p&gt;The “best” way to do this is if your CSS framework provides its styles as a set of &lt;a class="reference external" href="https://sass-lang.com/documentation/at-rules/mixin"&gt;Sass mixins&lt;/a&gt;, or something equivalent. Bulma, as it happens, usually does this, but sometimes we’re not so lucky, and we just have CSS.&lt;/p&gt;
&lt;p&gt;The trick I learnt requires you to use Sass/SCSS and the &lt;a class="reference external" href="https://sass-lang.com/documentation/at-rules/extend"&gt;@extend directive&lt;/a&gt;. This powerful directive takes rules relating to one selector, and pulls them into whatever rule you are writing.&lt;/p&gt;
&lt;p&gt;(If you are, like me, put off using things like CSS pre-processors because of the need for a separate build step, or needing to use Node.js/npm, see my post on &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step"&gt;How to use Sass/SCSS in a Django project without needing Node.js/npm or running a build process&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The one thing you have to do is rename the base CSS file you want to re-use from &lt;code class="docutils literal"&gt;.css&lt;/code&gt; to &lt;code class="docutils literal"&gt;.scss&lt;/code&gt;. This works because SCSS is a CSS superset. Then, for the example above, you can write your own SCSS file like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code scss"&gt;&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-1" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-1" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;@import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path/to/bulma.scss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-2" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-2" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-3" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-3" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-3"&gt;&lt;/a&gt;&lt;span class="nc"&gt;.field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="nc"&gt;.errorlist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-4" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-4" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-4"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@extend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;.has-text-danger&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-5" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-5" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-6" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-6" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-7" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-7" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-7"&gt;&lt;/a&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="nc"&gt;.errorlist.nonfield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-8" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-8" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;@extend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;.notification&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-9" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-9" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-9"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;extend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;.is-danger&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-10" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-10" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-10"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;extend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;.is-light&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;a id="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-11" name="rest_code_17f244ecdfa5404e8560c4fb8f6367c1-11" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_17f244ecdfa5404e8560c4fb8f6367c1-11"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This technique can be very powerful e.g. make all &lt;code class="docutils literal"&gt;input[type=text]&lt;/code&gt; inside a &lt;code class="docutils literal"&gt;&amp;lt;form &lt;span class="pre"&gt;class="bulma"&amp;gt;&lt;/span&gt;&lt;/code&gt; have the normal Bulma &lt;a class="reference external" href="https://bulma.io/documentation/form/input/"&gt;input&lt;/a&gt; appearance:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code scss"&gt;&lt;a id="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-1" name="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-1" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_3dcce6e72dbf4c2e96d3c0e087160480-1"&gt;&lt;/a&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="nc"&gt;.bulma&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-2" name="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-2" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_3dcce6e72dbf4c2e96d3c0e087160480-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;text&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;a id="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-3" name="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-3" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_3dcce6e72dbf4c2e96d3c0e087160480-3"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;@extend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;.input&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;a id="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-4" name="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-4" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_3dcce6e72dbf4c2e96d3c0e087160480-4"&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_3dcce6e72dbf4c2e96d3c0e087160480-5" name="rest_code_3dcce6e72dbf4c2e96d3c0e087160480-5" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_3dcce6e72dbf4c2e96d3c0e087160480-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will include all related rules like &lt;code class="docutils literal"&gt;.input:focus&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;As mentioned, it may not always be the best technique, but it’s a great one to have in your toolbox.&lt;/p&gt;</content>
    <category term="django" label="Django"/>
    <category term="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>Django and Sass/SCSS without Node.js or a build step</title>
    <id>https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/</id>
    <updated>2023-06-01T19:54:15Z</updated>
    <published>2023-06-01T19:54:15Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/"/>
    <summary type="html">&lt;p&gt;How to use Sass/SCSS in a Django project, without needing Node.js/npm or running a build process&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Although they are less necessary than in the past, I like to use a &lt;a class="reference external" href="https://developer.mozilla.org/en-US/docs/Glossary/CSS_preprocessor"&gt;CSS pre-processor&lt;/a&gt; when doing web development. I used to use &lt;a class="reference external" href="https://lesscss.org/"&gt;LessCSS&lt;/a&gt;, but recently I’ve found that I can use &lt;a class="reference external" href="https://sass-lang.com/"&gt;Sass&lt;/a&gt; without needing either a separate build step, or a package that requires Node.js and npm to install it. The heart of the functionality is provided by &lt;a class="reference external" href="https://sass-lang.com/libsass"&gt;libsass&lt;/a&gt;, an implementation of Sass as a C++ library.&lt;/p&gt;
&lt;p&gt;On Linux systems, this can be installed as a package &lt;code class="docutils literal"&gt;libsass&lt;/code&gt; or similar, but even better is that you can pip install it as a Python package, &lt;a class="reference external" href="https://pypi.org/project/libsass/"&gt;libsass&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When it comes to using it from a Django project, the first step is to &lt;a class="reference external" href="https://django-compressor.readthedocs.io/en/stable/quickstart.html"&gt;install
django-compressor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then, you need to add &lt;a class="reference external" href="https://pypi.org/project/django-libsass/"&gt;django-libsass&lt;/a&gt; as per its instructions.&lt;/p&gt;
&lt;p&gt;That’s about it. As per the django-libsass instructions, somewhere in your base HTML templates you’ll have something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code html+django"&gt;&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-1" name="rest_code_45cc202ef7a5489a9480af6509be422e-1" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-1"&gt;&lt;/a&gt;&lt;span class="c"&gt;{# at the top #}&lt;/span&gt;
&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-2" name="rest_code_45cc202ef7a5489a9480af6509be422e-2" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-2"&gt;&lt;/a&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;load&lt;/span&gt; &lt;span class="nv"&gt;compress&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-3" name="rest_code_45cc202ef7a5489a9480af6509be422e-3" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-3"&gt;&lt;/a&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;load&lt;/span&gt; &lt;span class="nv"&gt;static&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-4" name="rest_code_45cc202ef7a5489a9480af6509be422e-4" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-5" name="rest_code_45cc202ef7a5489a9480af6509be422e-5" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-5"&gt;&lt;/a&gt;{# in the &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; element #]
&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-6" name="rest_code_45cc202ef7a5489a9480af6509be422e-6" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-6"&gt;&lt;/a&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;compress&lt;/span&gt; &lt;span class="nv"&gt;css&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-7" name="rest_code_45cc202ef7a5489a9480af6509be422e-7" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-7"&gt;&lt;/a&gt;  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text/x-scss"&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="s2"&gt;"myapp/css/main.scss"&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_45cc202ef7a5489a9480af6509be422e-8" name="rest_code_45cc202ef7a5489a9480af6509be422e-8" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_45cc202ef7a5489a9480af6509be422e-8"&gt;&lt;/a&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endcompress&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You write your SCSS in that &lt;code class="docutils literal"&gt;main.scss&lt;/code&gt; file (it doesn’t have to be called that), and it can &lt;code class="docutils literal"&gt;@import&lt;/code&gt; other SCSS files of course.&lt;/p&gt;
&lt;p&gt;Then, when you load a page, django-compressor will take care of running the SCSS files through libsass, saving the output CSS to a file and inserting the appropriate HTML that references that CSS file into your template output. It caches things very well so that you don’t incur any penalty if files haven’t changed — and libsass is a very fast implementation for when the processing does need to happen.&lt;/p&gt;
&lt;p&gt;What this means is that you have eliminated both the need for Node.js/npm, and the need for a build step/process, if you only needed these things for CSS pre-processing.&lt;/p&gt;
&lt;p&gt;Of course, the SCSS → CSS compilation still has to happen, but it happens on demand in the same process that runs the web app, and it’s both fast enough and reliable enough that you simply never have to think about it again. So this is “build-less” in the same way that “server-less” means you don’t have to think about servers, and the same way that Python “doesn’t have a compilation step”.&lt;/p&gt;
&lt;section id="future-proofing"&gt;
&lt;h2&gt;Future proofing&lt;/h2&gt;
&lt;p&gt;On the Sass-lang page about libsass, they say it is “deprecated”, and on the &lt;a class="reference external" href="https://github.com/sass/libsass"&gt;project page&lt;/a&gt; page it says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While it will continue to receive maintenance releases indefinitely, there are no plans to add additional features or compatibility with any new CSS or Sass features.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In other words, this is what I prefer to call “mature software” 😉. libsass already has everything I need. If it does eventually fail to be maintained or I need new features, it’s not a problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Switch to Dart Sass, which can be installed as a &lt;a class="reference external" href="https://github.com/sass/dart-sass/releases/"&gt;standalone binary&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set your django-compressor settings like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_bf71ba731db14ec48efdaa29702f373b-1" name="rest_code_bf71ba731db14ec48efdaa29702f373b-1" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_bf71ba731db14ec48efdaa29702f373b-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;COMPRESS_PRECOMPILERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;a id="rest_code_bf71ba731db14ec48efdaa29702f373b-2" name="rest_code_bf71ba731db14ec48efdaa29702f373b-2" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_bf71ba731db14ec48efdaa29702f373b-2"&gt;&lt;/a&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"text/x-scss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sass &lt;/span&gt;&lt;span class="si"&gt;{infile}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{outfile}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_bf71ba731db14ec48efdaa29702f373b-3" name="rest_code_bf71ba731db14ec48efdaa29702f373b-3" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_bf71ba731db14ec48efdaa29702f373b-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This covers the basic case. If you want all the features of django-libsass, which includes looking in your other static file folders for SCSS, you’ll probably need to fork &lt;a class="reference external" href="https://github.com/torchbox/django-libsass/blob/main/django_libsass.py"&gt;the code&lt;/a&gt; and make it work by calling Dart Sass using &lt;a class="reference external" href="https://docs.python.org/3/library/subprocess.html"&gt;subprocess&lt;/a&gt; — a small amount of work, and nothing that will fundamentally break this approach.&lt;/p&gt;
&lt;/section&gt;</content>
    <category term="django" label="Django"/>
    <category term="python" label="Python"/>
    <category term="web-development" label="Web development"/>
  </entry>
  <entry>
    <title>Test smarter, not harder</title>
    <id>https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/</id>
    <updated>2020-09-04T19:46:50+01:00</updated>
    <published>2020-09-04T19:46:50+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/"/>
    <summary type="html">&lt;p&gt;Tips for winning the automated testing battle.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;“Smarter, not harder” is a saying used in many contexts, but rowing is the
context I think I first heard it in, and I still associate it with rowing many
years later.&lt;/p&gt;
&lt;p&gt;When you look at novice and more experienced rowing crews, it seems particularly
appropriate, because the primary difference is not the amount of effort that
goes in, nor even the strength of the rowers, but technique. Poor rowers still
finish a race absolutely exhausted, but they've moved at a fraction of the speed
of better crews. Sometimes the effort they put in actually slows the boat down.
They tend to make a lot of noise, splash a huge amount of water in every
direction, and pull a lot of faces. (I did a lot of all those things when I tried
rowing!).&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://youtu.be/6V6va2RIdeE?t=4327"&gt;Expert crews, however, do none of these things&lt;/a&gt;, because they don't make you go faster.
These rowers do a huge amount of training, and exercise massive amounts of
concentration, to ensure that every bit of the (very large) effort they put in
is actually contributing to speed.&lt;/p&gt;
&lt;p&gt;The “smarter not harder” mindset is also essential for writing good automated
software tests.&lt;/p&gt;
&lt;p&gt;It's in this context that religious devotion to things like &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Test-driven_development"&gt;TDD&lt;/a&gt; can be really
unhelpful. For many religions, the more painful an activity, and the more you do
it, the more meritorious it is – and it may even atone for past misdeeds. If you
take that mindset with you into writing tests, you will do a rather bad job.&lt;/p&gt;
&lt;p&gt;If writing tests is extremely painful, it may be a sign that something is wrong.
Huge and unnecessary quantities of tests are not meritorious, they are a massive
maintenance burden. Many of the things that make tests hard to write are also
going to make them hard (and therefore expensive) to maintain. I've seen far too
many examples where it looks like people have just sat back and accepted their
painful fate.&lt;/p&gt;
&lt;p&gt;For example, good ol' Uncle Bob seems to have this attitude. He &lt;a class="reference external" href="https://blog.cleancoder.com/uncle-bob/2017/01/11/TheDarkPath.html"&gt;wrote&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;you’d better get used to writing lots and lots of tests, no matter what
language you are using!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.hillelwayne.com/post/uncle-bob/"&gt;Don't listen to Uncle Bob!&lt;/a&gt; (at
least, not on this subject).&lt;/p&gt;
&lt;p&gt;“Test smarter, not harder” means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Only write necessary tests – specifically, tests whose estimated value is
greater than their estimated cost. This is a hard judgement call, of course,
but it does mean that at least some of the time you should be saying “it's not
worth it”. Some of the costs associated with tests are:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;the time taken to write them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the time they add to the test suite on every run.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the time to maintain them - understand them, debug them, change them when
other things change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;every time they fail incorrectly - when the functionality works, but the
test fails.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The value on the other hand, is found in:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;catching regressions, and doing so at low cost with a quick feedback loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;enabling fearless refactoring (which is a consequence of the above, but
distinct from it).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;providing a starting point for making changes, including a form of
documentation for the existing desirable behaviour.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write your test code with the functions/methods/classes you wish existed, not
the ones you've been given. For example, don't write this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_8bd76c201756420999f09be968c77da4-1" name="rest_code_8bd76c201756420999f09be968c77da4-1" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8bd76c201756420999f09be968c77da4-1"&gt;&lt;/a&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&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="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;live_server_url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"contact_form"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;a id="rest_code_8bd76c201756420999f09be968c77da4-2" name="rest_code_8bd76c201756420999f09be968c77da4-2" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8bd76c201756420999f09be968c77da4-2"&gt;&lt;/a&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_element_by_css_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#id_email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"my@email.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_8bd76c201756420999f09be968c77da4-3" name="rest_code_8bd76c201756420999f09be968c77da4-3" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8bd76c201756420999f09be968c77da4-3"&gt;&lt;/a&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_element_by_css_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#id_message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_8bd76c201756420999f09be968c77da4-4" name="rest_code_8bd76c201756420999f09be968c77da4-4" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8bd76c201756420999f09be968c77da4-4"&gt;&lt;/a&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_element_by_css_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"input[type=submit]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_8bd76c201756420999f09be968c77da4-5" name="rest_code_8bd76c201756420999f09be968c77da4-5" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8bd76c201756420999f09be968c77da4-5"&gt;&lt;/a&gt;&lt;span class="n"&gt;WebDriverWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find_element_by_css_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That looks very tedious! Write this instead:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_8e0a9719aa6740d18f0ba837187d5a42-1" name="rest_code_8e0a9719aa6740d18f0ba837187d5a42-1" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8e0a9719aa6740d18f0ba837187d5a42-1"&gt;&lt;/a&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"contact_form"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_8e0a9719aa6740d18f0ba837187d5a42-2" name="rest_code_8e0a9719aa6740d18f0ba837187d5a42-2" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8e0a9719aa6740d18f0ba837187d5a42-2"&gt;&lt;/a&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;a id="rest_code_8e0a9719aa6740d18f0ba837187d5a42-3" name="rest_code_8e0a9719aa6740d18f0ba837187d5a42-3" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8e0a9719aa6740d18f0ba837187d5a42-3"&gt;&lt;/a&gt;    &lt;span class="s2"&gt;"#id_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"my@email.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_8e0a9719aa6740d18f0ba837187d5a42-4" name="rest_code_8e0a9719aa6740d18f0ba837187d5a42-4" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8e0a9719aa6740d18f0ba837187d5a42-4"&gt;&lt;/a&gt;    &lt;span class="s2"&gt;"#id_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_8e0a9719aa6740d18f0ba837187d5a42-5" name="rest_code_8e0a9719aa6740d18f0ba837187d5a42-5" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8e0a9719aa6740d18f0ba837187d5a42-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;a id="rest_code_8e0a9719aa6740d18f0ba837187d5a42-6" name="rest_code_8e0a9719aa6740d18f0ba837187d5a42-6" href="https://lukeplant.me.uk/blog/posts/test-smarter-not-harder/#rest_code_8e0a9719aa6740d18f0ba837187d5a42-6"&gt;&lt;/a&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"input[type=submit]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(Like you can with &lt;a class="reference external" href="https://django-functest.readthedocs.io/en/latest/"&gt;django-functest&lt;/a&gt;, but it's the principle,
not the library, that's important. If the API you want to use doesn't exist
yet, you still use it, and then make it exist.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don't write tests for things that can be more effectively tested in other
ways, and lean on other correctness methodologies as much as possible. These
include:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;code review&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;static type checking (especially in languages with sound and powerful type
systems, with type inference everywhere, giving you a very good cost-benefit
ratio)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;linters like &lt;a class="reference external" href="https://github.com/pycqa/flake8"&gt;flake8&lt;/a&gt; and &lt;a class="reference external" href="https://semgrep.dev/"&gt;Semgrep&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://www.hillelwayne.com/post/business-case-formal-methods/"&gt;formal methods&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;introspection (like &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/topics/checks/#module-django.core.checks"&gt;Django's checks framework&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;property based testing like &lt;a class="reference external" href="https://hypothesis.readthedocs.io/en/latest/"&gt;hypothesis&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move the burden onto the computer. “Push the loop in”.&lt;/p&gt;
&lt;p&gt;Take, for example, a requirement that every entry point to your web app (i.e.
a page or HTTP API), apart from a few exceptions like login and reset
password, should require authentication.&lt;/p&gt;
&lt;p&gt;The “test harder” religion interprets this as:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;For every entry point&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Write a test that&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ensures non-authenticated requests return 403&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's a lot of tests, and even worse is that you have to remember to write
them.&lt;/p&gt;
&lt;p&gt;“Test smarter” says:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Write a test that&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;For every entry point&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ensures non-authenticated requests return 403&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's one test. “Write a test” is executed in developer time, so in the first
example the loop ("For every entry point") is also executed in developer time.
Push the loop inside the test, and it gets executed in computer time instead.&lt;/p&gt;
&lt;p&gt;Already mentioned, but &lt;a class="reference external" href="https://hypothesis.readthedocs.io/en/latest/"&gt;hypothesis&lt;/a&gt; is a great way to push the
loop in. Also, the implementation of the requirements can benefit from the
same techniques that the tests do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cheat on your homework. It's smart to get help, and hard work is for suckers.
If you have a good idea, but don't know the techniques or tools you need to
implement it, or whether it is even possible (for example, in the example
above you don't know how to introspect your system to get a list of all entry
points), there are a lot of smart people on &lt;a class="reference external" href="https://stackoverflow.com/"&gt;StackOverflow&lt;/a&gt; who will revel in the challenge.&lt;/p&gt;
&lt;p&gt;(Level up: loudly claim on Twitter that "it appears to be impossible to X with
tool Y" and know-it-alls like me will magically appear with solutions).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course, there are still times when hard work is required for writing tests —
times when it will be tedious, and times when our instincts to skimp are
actually misplaced laziness that will cost more in the long run. But you should
hustle and cheat your way out of unnecessary effort as much as you possibly can.
Your overall testing strategy should feel like “I get that computer to do so
much work for me!”, not ”My RSI and bleeding fingers have hopefully appeased the
testing gods and atoned for my previous omissions”.&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://www.reddit.com/r/programming/comments/imzawj/test_smarter_not_harder/"&gt;Discussion on this post on Reddit&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://lobste.rs/s/hit4t9/test_smarter_not_harder"&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>Announcement: Django Views - The Right Way</title>
    <id>https://lukeplant.me.uk/blog/posts/announcement-django-views-the-right-way/</id>
    <updated>2020-08-19T21:51:36+01:00</updated>
    <published>2020-08-19T21:51:36+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/announcement-django-views-the-right-way/"/>
    <summary type="html">&lt;p&gt;Announcement of my guide to writing Django Views.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;I announced this a few days back on Twitter, this is just a quick additional
blog post to announce &lt;a class="reference external" href="https://spookylukey.github.io/django-views-the-right-way/"&gt;Django Views - The Right Way&lt;/a&gt;. It's an
opinionated guide to writing views in Django that I've been working on for a few
months.&lt;/p&gt;
&lt;p&gt;This project turned out to be much bigger than I expected. And in the end, more
about general programming and Python principles than just Django – so you may
enjoy it even if you're not into Django.&lt;/p&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>
</feed>
