<?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 Django)</title>
  <id>https://lukeplant.me.uk/blog/categories/django.xml</id>
  <updated>2026-08-02T07:32:44Z</updated>
  <author>
    <name>Luke Plant</name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://lukeplant.me.uk/blog/categories/django.xml"/>
  <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/categories/django/"/>
  <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>pyastgrep and custom linting</title>
    <id>https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/</id>
    <updated>2024-05-23T20:07:34+01:00</updated>
    <published>2024-05-23T20:07:34+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/"/>
    <summary type="html">&lt;p&gt;Methodology and sample code for using pyastgrep to do custom linting tasks on Python source code.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;A while back I released &lt;a class="reference external" href="https://github.com/spookylukey/pyastgrep/"&gt;pyastgrep&lt;/a&gt;, which is a rewrite of &lt;a class="reference external" href="https://github.com/hchasestevens/astpath"&gt;astpath&lt;/a&gt;. It’s a tool that allows you to search for specific Python syntax elements using &lt;a class="reference external" href="https://en.wikipedia.org/wiki/XPath"&gt;XPath&lt;/a&gt; as a query language.&lt;/p&gt;
&lt;p&gt;As part of the rewrite, I separated out the layers of code so that it can now be used &lt;a class="reference external" href="https://pyastgrep.readthedocs.io/en/latest/library.html"&gt;as a library&lt;/a&gt; as well as a command line tool. I haven’t committed to very much API surface area for library usage, but there is enough.&lt;/p&gt;
&lt;p&gt;My main personal use of this has been for linting tasks or enforcing of conventions that might be difficult to do otherwise. I don’t always use this – quite often I’d reach for &lt;a class="reference external" href="https://semgrep.dev/docs/writing-rules/rule-ideas"&gt;custom Semgrep rules&lt;/a&gt;, and at other times &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/"&gt;I use introspection to enforce conventions&lt;/a&gt;. However, there are times when both of these fail or are rather difficult.&lt;/p&gt;
&lt;section id="examples"&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;p&gt;Some examples of the kinds of rules I’m thinking of include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Boolean arguments to functions/methods should always be “keyword only”.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/keyword-only-arguments-in-python/"&gt;Keyword-only arguments are a big win in many cases&lt;/a&gt;, and especially when it
comes to boolean values. For example, forcing &lt;code class="docutils literal"&gt;delete_thing(True, False)&lt;/code&gt;
to be something like &lt;code class="docutils literal"&gt;delete_thing(permanent=True, force=False)&lt;/code&gt; is an easy
win, and this is common enough that applying this as a default policy across
the code base will probably be a good idea.&lt;/p&gt;
&lt;p&gt;The pattern can be distinguished easily at syntax level. Good:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_fbe210cec1654628b3e282a86aacc0b6-1" name="rest_code_fbe210cec1654628b3e282a86aacc0b6-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_fbe210cec1654628b3e282a86aacc0b6-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;my_bool_arg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_fbe210cec1654628b3e282a86aacc0b6-2" name="rest_code_fbe210cec1654628b3e282a86aacc0b6-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_fbe210cec1654628b3e282a86aacc0b6-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_aa4018d78510461590a6bf2947d9fd1e-1" name="rest_code_aa4018d78510461590a6bf2947d9fd1e-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_aa4018d78510461590a6bf2947d9fd1e-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_bool_arg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_aa4018d78510461590a6bf2947d9fd1e-2" name="rest_code_aa4018d78510461590a6bf2947d9fd1e-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_aa4018d78510461590a6bf2947d9fd1e-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simple coding conventions like “Don’t use single letter variables like &lt;code class="docutils literal"&gt;i&lt;/code&gt; or &lt;code class="docutils literal"&gt;j&lt;/code&gt; as a loop variables, use &lt;code class="docutils literal"&gt;index&lt;/code&gt; or &lt;code class="docutils literal"&gt;idx&lt;/code&gt; instead”.&lt;/p&gt;
&lt;p&gt;This can be found by looking for code like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_bb613ed294b54b78a4bf75f1ca83981a-1" name="rest_code_bb613ed294b54b78a4bf75f1ca83981a-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_bb613ed294b54b78a4bf75f1ca83981a-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_bb613ed294b54b78a4bf75f1ca83981a-2" name="rest_code_bb613ed294b54b78a4bf75f1ca83981a-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_bb613ed294b54b78a4bf75f1ca83981a-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You might not care about this, but if you do, you really want the rule to be applied as an automated test, not a nit-picky code review.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Django-specific one: for &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#howto-custom-template-tags-inclusion-tags"&gt;inclusion tags&lt;/a&gt;,
the tag names should match the template file name. This is nice for
consistency and code navigation, plus I actually have some custom “jump to
definition” code in my editor that relies on it for fast navigation.&lt;/p&gt;
&lt;p&gt;The pattern can again be seen quite easily at the syntax level. Good:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_cf490325204943159cefea87ba497c75-1" name="rest_code_cf490325204943159cefea87ba497c75-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cf490325204943159cefea87ba497c75-1"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@inclusion_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"something/foo.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_cf490325204943159cefea87ba497c75-2" name="rest_code_cf490325204943159cefea87ba497c75-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cf490325204943159cefea87ba497c75-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_cf490325204943159cefea87ba497c75-3" name="rest_code_cf490325204943159cefea87ba497c75-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cf490325204943159cefea87ba497c75-3"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_899d59f9c8784c43bac7356d38727372-1" name="rest_code_899d59f9c8784c43bac7356d38727372-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_899d59f9c8784c43bac7356d38727372-1"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@inclusion_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"something/bar.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_899d59f9c8784c43bac7356d38727372-2" name="rest_code_899d59f9c8784c43bac7356d38727372-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_899d59f9c8784c43bac7356d38727372-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_899d59f9c8784c43bac7356d38727372-3" name="rest_code_899d59f9c8784c43bac7356d38727372-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_899d59f9c8784c43bac7356d38727372-3"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any ’task’ (something decorated with &lt;code class="docutils literal"&gt;@task&lt;/code&gt;) should be named &lt;code class="docutils literal"&gt;foo_task&lt;/code&gt;
in order to give a clue that it works as an asynchronous call, and its return
value is just a promise object.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are many more examples you’ll come up with once you start thinking like this.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="method"&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;p&gt;Having identified the bad patterns we want to find and fix, my method for doing so looks as follows. It contains a number of tips and refinements I’ve made over the past few years.&lt;/p&gt;
&lt;p&gt;First, I open a test file, e.g. &lt;code class="docutils literal"&gt;tests/test_conventions.py&lt;/code&gt;, and start by inserting some example code – at least one bad example (the kind we are trying to fix), and one good example.&lt;/p&gt;
&lt;p&gt;There are a few reasons for this:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;First, I need to make sure &lt;a class="reference external" href="https://www.johndcook.com/blog/2018/05/26/proving-life-on-earth/"&gt;I can prove life exists on earth&lt;/a&gt;, as John D. Cook puts it. I’ll say more about this later on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, it gives me a deliberately simplified bit of code that I can pass to &lt;a class="reference external" href="https://pyastgrep.readthedocs.io/en/latest/usage.html#understanding-the-xml-structure"&gt;pyastdump&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third, it provides some explanation for the test I’m going to write, and a potentially rather hairy XPath expression.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ll use my first example above, keyword-only boolean args. I start by inserting the following text into my test file:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_467aeb9fb66d480585779d67000db35e-1" name="rest_code_467aeb9fb66d480585779d67000db35e-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_467aeb9fb66d480585779d67000db35e-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_boolean_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_467aeb9fb66d480585779d67000db35e-2" name="rest_code_467aeb9fb66d480585779d67000db35e-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_467aeb9fb66d480585779d67000db35e-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;a id="rest_code_467aeb9fb66d480585779d67000db35e-3" name="rest_code_467aeb9fb66d480585779d67000db35e-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_467aeb9fb66d480585779d67000db35e-3"&gt;&lt;/a&gt;
&lt;a id="rest_code_467aeb9fb66d480585779d67000db35e-4" name="rest_code_467aeb9fb66d480585779d67000db35e-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_467aeb9fb66d480585779d67000db35e-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_467aeb9fb66d480585779d67000db35e-5" name="rest_code_467aeb9fb66d480585779d67000db35e-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_467aeb9fb66d480585779d67000db35e-5"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;good_boolean_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_467aeb9fb66d480585779d67000db35e-6" name="rest_code_467aeb9fb66d480585779d67000db35e-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_467aeb9fb66d480585779d67000db35e-6"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, I copy both of these in turn to the clipboard (or both together if there isn’t much code, like in this case), and pass them through &lt;code class="docutils literal"&gt;pyastdump&lt;/code&gt;. From a terminal, I do:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_e3f89a626cc74c5080836c0110d70e7b-1" name="rest_code_e3f89a626cc74c5080836c0110d70e7b-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_e3f89a626cc74c5080836c0110d70e7b-1"&gt;&lt;/a&gt;$&lt;span class="w"&gt; &lt;/span&gt;xsel&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pyastdump&lt;span class="w"&gt; &lt;/span&gt;-
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I’m using the &lt;code class="docutils literal"&gt;xsel&lt;/code&gt; Linux utility, you can also use &lt;code class="docutils literal"&gt;xclip &lt;span class="pre"&gt;-out&lt;/span&gt;&lt;/code&gt;, or &lt;code class="docutils literal"&gt;pbpaste&lt;/code&gt; on MacOS, or &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;Get-Clipboard&lt;/span&gt;&lt;/code&gt; in Powershell.&lt;/p&gt;
&lt;p&gt;This gives me some AST to look at, structured as XML:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code xml"&gt;&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-1" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-1"&gt;&lt;/a&gt;&lt;span class="nt"&gt;&amp;lt;Module&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-2" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-3" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-3"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;FunctionDef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"str"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"bad_boolean_arg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-4" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-4"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-5" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-5"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;arguments&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-6" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-6"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;posonlyargs/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-7" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-7"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-8" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;arg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"str"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;arg=&lt;/span&gt;&lt;span class="s"&gt;"foo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-9" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-9" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-9"&gt;&lt;/a&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;annotation&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-10" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-10" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-10"&gt;&lt;/a&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"25"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"str"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bool"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-11" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-11" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-11"&gt;&lt;/a&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;ctx&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-12" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-12" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-12"&gt;&lt;/a&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Load/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-13" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-13" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-13"&gt;&lt;/a&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/ctx&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-14" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-14" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-14"&gt;&lt;/a&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-15" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-15" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-15"&gt;&lt;/a&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/annotation&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-16" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-16" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-16"&gt;&lt;/a&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/arg&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-17" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-17" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-17"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-18" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-18" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-18"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;kwonlyargs/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-19" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-19" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-19"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;kw_defaults/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-20" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-20" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-20"&gt;&lt;/a&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;defaults/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-21" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-21" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-21"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-22" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-22" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-22"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/args&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-23" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-23" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-23"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-24" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-24" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-24"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;lineno=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;col_offset=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-25" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-25" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-25"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-26" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-26" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-26"&gt;&lt;/a&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;decorator_list/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-27" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-27" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-27"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/FunctionDef&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-28" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-28" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-28"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-29" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-29" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-29"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;type_ignores/&amp;gt;&lt;/span&gt;
&lt;a id="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-30" name="rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-30" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9ec65d94b22f4ee0b1a01a9d07b9b084-30"&gt;&lt;/a&gt;&lt;span class="nt"&gt;&amp;lt;/Module&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this case, the current structure of Python’s AST has helped us out a lot – it has separated out &lt;code class="docutils literal"&gt;posonlyargs&lt;/code&gt; (positional only arguments), &lt;code class="docutils literal"&gt;args&lt;/code&gt; (positional or keyword), and &lt;code class="docutils literal"&gt;kwonlyargs&lt;/code&gt; (keyword only args). We can see the offending &lt;code class="docutils literal"&gt;annotation&lt;/code&gt; containing a &lt;code class="docutils literal"&gt;Name&lt;/code&gt; with &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;id="bool"&lt;/span&gt;&lt;/code&gt; inside the &lt;code class="docutils literal"&gt;args&lt;/code&gt;, when we want it only to be allowed as a keyword-only argument.&lt;/p&gt;
&lt;p&gt;(Do we want to disallow boolean-annotated arguments as positional only? I’m leaning towards “no” here, as positional only is quite rare and usually a very deliberate choice).&lt;/p&gt;
&lt;p&gt;I now have to construct an XPath expression that will find the offending XML nodes, but not match good examples. It’s pretty straightforward in this case, once you know the basics of XPath. I test it out straight away at the CLI:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_1b90770b43ac44ceb07858e8d2f1d7d9-1" name="rest_code_1b90770b43ac44ceb07858e8d2f1d7d9-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_1b90770b43ac44ceb07858e8d2f1d7d9-1"&gt;&lt;/a&gt;pyastgrep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'.//FunctionDef/args/arguments/args/arg/annotation/Name[@id="bool"]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tests/test_conventions.py
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If I’ve done it correctly, it should print my bad example, and not my good example.&lt;/p&gt;
&lt;p&gt;Then I widen the net, omitting &lt;code class="docutils literal"&gt;tests/test_conventions.py&lt;/code&gt; to search everywhere in my current directory.&lt;/p&gt;
&lt;p&gt;At this point, I’ve probably got some real results that I want to address, but I might also notice there are other variants of the same thing I need to be able to match, and so I iterate, adding more bad/good examples as necessary.&lt;/p&gt;
&lt;p&gt;Now I need to write a test. It’s going to look like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-1" name="rest_code_64217224e6e6446b8cc44f1026d7f483-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_boolean_arguments_are_keyword_only&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-2" name="rest_code_64217224e6e6446b8cc44f1026d7f483-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;assert_expected_pyastgrep_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-3" name="rest_code_64217224e6e6446b8cc44f1026d7f483-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-3"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-4" name="rest_code_64217224e6e6446b8cc44f1026d7f483-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-4"&gt;&lt;/a&gt;&lt;span class="sd"&gt;        .//FunctionDef/args/arguments/args/arg/annotation/Name[@id="bool"]&lt;/span&gt;
&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-5" name="rest_code_64217224e6e6446b8cc44f1026d7f483-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-5"&gt;&lt;/a&gt;&lt;span class="sd"&gt;        """&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-6" name="rest_code_64217224e6e6446b8cc44f1026d7f483-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-6"&gt;&lt;/a&gt;        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Function arguments with type `bool` should be keyword-only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-7" name="rest_code_64217224e6e6446b8cc44f1026d7f483-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-7"&gt;&lt;/a&gt;        &lt;span class="n"&gt;expected_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_64217224e6e6446b8cc44f1026d7f483-8" name="rest_code_64217224e6e6446b8cc44f1026d7f483-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_64217224e6e6446b8cc44f1026d7f483-8"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Of course, the real work is being done inside my &lt;code class="docutils literal"&gt;assert_expected_pyastgrep_matches&lt;/code&gt; utility, which looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-1" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-2" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;boltons&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;iterutils&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-3" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyastgrep.api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search_python_files&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-4" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-5" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-5"&gt;&lt;/a&gt;&lt;span class="n"&gt;SRC_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# depends on project structure&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-6" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-7" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-8" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-8"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_expected_pyastgrep_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xpath_expr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-9" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-9" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-9"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-10" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-10" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-10"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Asserts that the pyastgrep XPath expression matches only `expected_count` times,&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-11" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-11" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-11"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    each of which must be marked with `pyastgrep_exception`&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-12" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-12" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-12"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-13" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-13" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-13"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    `message` is a message to be printed on failure.&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-14" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-14" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-14"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-15" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-15" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-15"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-16" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-16" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-16"&gt;&lt;/a&gt;    &lt;span class="n"&gt;xpath_expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xpath_expr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-17" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-17" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-17"&gt;&lt;/a&gt;    &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;search_python_files&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;SRC_ROOT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;xpath_expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-18" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-18" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-18"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-19" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-19" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-19"&gt;&lt;/a&gt;    &lt;span class="n"&gt;expected_matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other_matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iterutils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-20" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-20" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-20"&gt;&lt;/a&gt;        &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"pyastgrep: expected"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matching_line&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-21" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-21" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-21"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-22" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-22" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-22"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-23" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-23" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-23"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;expected_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-24" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-24" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-24"&gt;&lt;/a&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; matches but found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;xpath_expr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-25" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-25" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-25"&gt;&lt;/a&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-26" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-26" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-26"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;other_matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-27" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-27" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-27"&gt;&lt;/a&gt;        &lt;span class="n"&gt;message&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-28" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-28" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-28"&gt;&lt;/a&gt;        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; Failing examples:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-29" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-29" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-29"&gt;&lt;/a&gt;        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-30" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-30" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-30"&gt;&lt;/a&gt;            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineno&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col_offset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matching_line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-31" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-31" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-31"&gt;&lt;/a&gt;            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;other_matches&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-32" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-32" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-32"&gt;&lt;/a&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9b1518091e764631a91a6eb3bdf684a0-33" name="rest_code_9b1518091e764631a91a6eb3bdf684a0-33" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_9b1518091e764631a91a6eb3bdf684a0-33"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There is a bit of explaining to do now.&lt;/p&gt;
&lt;p&gt;Being sure that you can “find life on earth” is especially important for a negative test like this. It would be very easy to have an XPath query that you thought worked but didn’t, as it might just silently return zero results. In addition, Python’s AST is not stable – so a query that works now might stop working in the future.&lt;/p&gt;
&lt;p&gt;It’s like you have a machine that claims to be able to find needles in haystacks – when it comes back and says “no needles found”, do you believe it? To increase your confidence that everything works and continues to work, you place a few needles at locations that you know, then check that the machine is able to find those needles. When it claims “found exactly 2 needles”, and you can account for those, you’ve got much more confidence that it has indeed found the all the needles (no false negatives).&lt;/p&gt;
&lt;p&gt;Additionally, to ensure we don’t have false positives, you can also place some things in the haystack that are similar to needles, but slightly different, and ensure that these are not counted as needles.&lt;/p&gt;
&lt;p&gt;So, it’s important to leave my bad (and good) examples in there.&lt;/p&gt;
&lt;p&gt;But, I obviously don’t want the bad examples to cause the test to fail! In addition, I want a general mechanism for exceptions. A simple mechanism I’ve chosen is to add the text &lt;code class="docutils literal"&gt;pyastgrep: expected&lt;/code&gt; as a comment.&lt;/p&gt;
&lt;p&gt;So, I need to change my bad example like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_c56a518bad5747618c3ad313d7636db6-1" name="rest_code_c56a518bad5747618c3ad313d7636db6-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c56a518bad5747618c3ad313d7636db6-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_boolean_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# pyastgrep: expected&lt;/span&gt;
&lt;a id="rest_code_c56a518bad5747618c3ad313d7636db6-2" name="rest_code_c56a518bad5747618c3ad313d7636db6-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_c56a518bad5747618c3ad313d7636db6-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I also pass &lt;code class="docutils literal"&gt;expected_count=1&lt;/code&gt; to indicate that I expect to find at least one bad example (or more, if I’ve added more bad examples).&lt;/p&gt;
&lt;p&gt;Hopefully that explains everything &lt;code class="docutils literal"&gt;assert_expected_pyastgrep_matches&lt;/code&gt; does. A couple more notes:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;it uses &lt;a class="reference external" href="https://boltons.readthedocs.io/en/latest/"&gt;boltons&lt;/a&gt;, a pretty useful set of Python utilities&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it requires a &lt;code class="docutils literal"&gt;SRC_ROOT&lt;/code&gt; folder to be defined, which will depend on your project, and might be different depending on which folder(s) you want to apply the convention too.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, everything is set up, and I run the test for real, hopefully locating all the bad usages. I work through them and fix, then leave the test in.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="tips"&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;pyastgrep works strictly at the syntax level, so unlike Semgrep you might get caught out by aliases if you try match on specific names:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-1" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;foo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt;
&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-2" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;foo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;foo_bar&lt;/span&gt;
&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-3" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;foo&lt;/span&gt;
&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-4" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-5" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-5"&gt;&lt;/a&gt;&lt;span class="c1"&gt;# These all call the same function but look different in AST:&lt;/span&gt;
&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-6" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-6"&gt;&lt;/a&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-7" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-7"&gt;&lt;/a&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_cb61309d12da4d9faa2f25c655dc6a70-8" name="rest_code_cb61309d12da4d9faa2f25c655dc6a70-8" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_cb61309d12da4d9faa2f25c655dc6a70-8"&gt;&lt;/a&gt;&lt;span class="n"&gt;foo_bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is however, an advantage to this – you don’t need a real import to construct your bad examples, you can just use a Mock. e.g. for my &lt;code class="docutils literal"&gt;inclusion_tag&lt;/code&gt; example above, I have code like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_547616c2301548e68080d42ac299fcb8-1" name="rest_code_547616c2301548e68080d42ac299fcb8-1" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_547616c2301548e68080d42ac299fcb8-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mock&lt;/span&gt;
&lt;a id="rest_code_547616c2301548e68080d42ac299fcb8-2" name="rest_code_547616c2301548e68080d42ac299fcb8-2" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_547616c2301548e68080d42ac299fcb8-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_547616c2301548e68080d42ac299fcb8-3" name="rest_code_547616c2301548e68080d42ac299fcb8-3" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_547616c2301548e68080d42ac299fcb8-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_547616c2301548e68080d42ac299fcb8-4" name="rest_code_547616c2301548e68080d42ac299fcb8-4" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_547616c2301548e68080d42ac299fcb8-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_547616c2301548e68080d42ac299fcb8-5" name="rest_code_547616c2301548e68080d42ac299fcb8-5" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_547616c2301548e68080d42ac299fcb8-5"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@register&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inclusion_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"something/not_bad_tag.html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_547616c2301548e68080d42ac299fcb8-6" name="rest_code_547616c2301548e68080d42ac299fcb8-6" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_547616c2301548e68080d42ac299fcb8-6"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_tag&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;  &lt;span class="c1"&gt;# pyastgrep: expected&lt;/span&gt;
&lt;a id="rest_code_547616c2301548e68080d42ac299fcb8-7" name="rest_code_547616c2301548e68080d42ac299fcb8-7" href="https://lukeplant.me.uk/blog/posts/pyastgrep-and-custom-linting/#rest_code_547616c2301548e68080d42ac299fcb8-7"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can see the &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/fbc9609f6f5e10d8fd8ea15b0eb272d37869ed34/cciw/cciwmain/tests/test_conventions.py#L38"&gt;full code on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You might be able to use a mixture of techniques:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;A Semgrep rule avoids one set of bad patterns using some &lt;code class="docutils literal"&gt;thirdparty.func&lt;/code&gt;, and requiring everyone to use your own wrapper, which is then constructed in such a way to make it easier to apply a pyastgrep rule&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some introspection that produces a list of classes or functions to which some rule applies, then dynamically generates XPath expression to pass to pyastgrep.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Syntax level searching isn’t right for every job, but it can be a powerful addition to your toolkit, and with a decent query language like XPath, you can do a surprising amount. Have a look at the &lt;a class="reference external" href="https://pyastgrep.readthedocs.io/en/latest/examples.html"&gt;pyastgrep examples&lt;/a&gt; for inspiration!&lt;/p&gt;
&lt;/section&gt;
&lt;section id="links"&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://lobste.rs/s/rp9rzq/pyastgrep_custom_linting"&gt;Discussion of this post on Lobsters&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</content>
    <category term="django" label="Django"/>
    <category term="pyastgrep" label="pyastgrep"/>
    <category term="python" label="Python"/>
    <category term="software-development" label="Software development"/>
  </entry>
  <entry>
    <title>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>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>Python’s “Disappointing” Superpowers</title>
    <id>https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/</id>
    <updated>2023-02-01T13:44:15Z</updated>
    <published>2023-02-01T13:44:15Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/"/>
    <summary type="html">&lt;p&gt;A response to Hillel Wayne’s “I am disappointed by dynamic typing”&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In Hillel Wayne’s post &lt;a class="reference external" href="https://buttondown.email/hillelwayne/archive/i-am-disappointed-by-dynamic-typing/"&gt;“I am disappointed by dynamic typing”&lt;/a&gt;, he expresses his sense that the Python ecosystem doesn’t really make the most of the possibilities that Python provides as a dynamically typed language. This is an important subject, since every Python program pays a very substantial set of costs for Python’s highly dynamic nature, such as poor run-time performance, and maintainability issues. Are we we getting anything out of this tradeoff?&lt;/p&gt;
&lt;p&gt;I think Hillel makes some fair points, and this post is intended as a response rather than a rebuttal. Recently there has been a significant influence of static type systems which I think might be harmful. The static type system we have in the form of mypy/pyright (which is partly codified in &lt;a class="reference external" href="https://peps.python.org/pep-0484/"&gt;PEP 484&lt;/a&gt; and following) seems to be much too heavily inspired by what is possible to map to other languages, rather than the features that Python provides.&lt;/p&gt;
&lt;p&gt;(As a simple example to support that claim, consider the fact that Python has had support for keyword arguments since as long as I can remember, and for keyword-only arguments since Python 3.0. But  &lt;code class="docutils literal"&gt;typing.Callable&lt;/code&gt; has zero support for them, meaning they can’t be typed in a higher-order context. . This is bad, since they are a key part of Python’s excellent reputation for readability, and &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/keyword-only-arguments-in-python/"&gt;we want more keyword-only arguments, not fewer&lt;/a&gt;.
[&lt;strong&gt;EDIT:&lt;/strong&gt; it looks like there is &lt;a class="reference external" href="https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols"&gt;another way to do it&lt;/a&gt;, it’s just about 10 times more work, so the point kind of stands.]
I can give more examples, but that will have to wait for another blog post).&lt;/p&gt;
&lt;p&gt;I’m worried that a de-facto move away from dynamic stuff in the Python ecosystem, possibly motivated by those who use Python only because they have to, and just want to make it more like the C# or Java they are comfortable with, could leave us with the very worst of all worlds.&lt;/p&gt;
&lt;p&gt;However, I also think there are plenty of counter-examples to Hillel’s claim, and that’s what this post will explore.&lt;/p&gt;
&lt;p&gt;Hillel was specifically thinking about, in his own words:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;“runtime program manipulation”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“programs that take programs and output other programs”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“thinking of the whole runtime environment in the same way, where everything is a runtime construct”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…and he gave some examples that included things like:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;run-time type modification&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;introspection/manipulation of the stack&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;passing very differently typed objects through normal code to collect information about it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m going to give lots of examples of this kind of thing in Python, and they will all be &lt;strong&gt;real world&lt;/strong&gt; examples. This means that either I have used them myself to solve real problems, or I’m aware that other people are using them in significant numbers.&lt;/p&gt;
&lt;p&gt;Before I get going, there are some things to point out.&lt;/p&gt;
&lt;p&gt;First, I don’t have the exact examples Hillel is looking for – but that’s because the kind of problems I’ve needed to solve have not been exactly the same as his. My examples are all necessarily limited in scope: since Python allows unrestricted side-effects in any function, including IO and being able to modify other code, there are obviously limits into how well these techniques can work across large amounts of code.&lt;/p&gt;
&lt;p&gt;I do think, however, that my examples are in the same general region, and some of them very close. On both sides we’ve got to avoid semantic hair-splitting – you can argue that every time you use the &lt;code class="docutils literal"&gt;class&lt;/code&gt; keyword in Python you are doing “run-time type creation”, rather than “compile-time type creation”, because that’s how Python’s classes work. But that’s not what Hillel meant.&lt;/p&gt;
&lt;p&gt;Second, many of these more magical techniques involve what is called monkey patching. People are often confused about the difference between monkey patching and “dynamic meta-programming”, so I’ve prepared a handy flow chart for you:&lt;/p&gt;
&lt;img alt="Flow chart: Is this code I found a hacky monkey patch, or cool dynamic meta-programming?  Question: who wrote it? If “Me” - it’s “Dynamic meta-programming”, if “someone else”, it’s “hacky monkey patch”" class="align-center" src="https://lukeplant.me.uk/blogmedia/monkey_patch_or_dynamic_meta_programming.png"&gt;
&lt;p&gt;There are, however, many instances of advanced, dynamic techniques that never get to the point of the chart above, and that’s because you never know about them. What you know is that the code does something useful, and it does so reliably enough that you don’t need to know what techniques contributed to it. And this is, I think, the biggest problem in what Hillel is asking for. The best examples of these techniques will be reliable enough that they don’t draw attention to themselves, and you immediately take them for granted.&lt;/p&gt;
&lt;p&gt;Which is also to say that you cannot discount something I mention below just because it is so widely used that you, too, have taken it for granted – that would effectively be saying that the only examples that count are the ones that have proved to be so wild and wacky that everyone has decided they are a bad idea.&lt;/p&gt;
&lt;p&gt;Third, you might also discount these examples as  “just using features the language provides”, rather than “hyper-programming” or something exotic. On the one hand, it would be true, but also unfair in the context of this debate. The most obvious example is &lt;code class="docutils literal"&gt;eval&lt;/code&gt;. This is clearly a very powerful technique not available to many statically typed languages, and exactly the kind that Hillel is looking for – you are literally creating more of your program as your program is running. On the other hand, it’s nothing more than a builtin function.&lt;/p&gt;
&lt;p&gt;Finally, a number of these examples don’t involve “production” code i.e. the code is typically run only on developer machines or in CI. These still count, however – just like many of Hillel’s examples are in the area of testing. The reasons they still count are 1) developers are humans too, and solving their problems is still important and 2) the techniques used by developers on their own machines are useful in creating high quality code for running on other people’s machines, where we don’t want to incur the performance or robustness penalties of the techniques used.&lt;/p&gt;
&lt;p&gt;So, here are my examples. The majority are not my own code, but I’ve also taken the opportunity to do some fairly obvious bragging about cool things I’ve done in Python.&lt;/p&gt;
&lt;nav class="contents" id="examples" role="doc-toc"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#top"&gt;Examples&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/pythons-disappointing-superpowers/#gooey" id="toc-entry-1"&gt;Gooey&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/pythons-disappointing-superpowers/#werkzeugs-interactive-debugger" id="toc-entry-2"&gt;Werkzeug’s interactive debugger&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/pythons-disappointing-superpowers/#hybrid-attributes-in-sqlalchemy" id="toc-entry-3"&gt;Hybrid attributes in SQLAlchemy&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/pythons-disappointing-superpowers/#pony-orm" id="toc-entry-4"&gt;Pony ORM&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/pythons-disappointing-superpowers/#django" id="toc-entry-5"&gt;Django&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/pythons-disappointing-superpowers/#foreignkey" id="toc-entry-6"&gt;ForeignKey&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/pythons-disappointing-superpowers/#relatedmanager" id="toc-entry-7"&gt;RelatedManager&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/pythons-disappointing-superpowers/#manytomany-models" id="toc-entry-8"&gt;ManyToMany models&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/pythons-disappointing-superpowers/#consequences" id="toc-entry-9"&gt;Consequences&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/pythons-disappointing-superpowers/#baserow" id="toc-entry-10"&gt;Baserow&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/pythons-disappointing-superpowers/#cciw-data-retention-policy" id="toc-entry-11"&gt;CCiW data retention policy&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/pythons-disappointing-superpowers/#query-tracing" id="toc-entry-12"&gt;Query tracing&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/pythons-disappointing-superpowers/#time-machine-and-pyfakefs" id="toc-entry-13"&gt;time-machine and pyfakefs&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/pythons-disappointing-superpowers/#environment-detection" id="toc-entry-14"&gt;Environment detection&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/pythons-disappointing-superpowers/#fluent-compiler" id="toc-entry-15"&gt;fluent-compiler&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/pythons-disappointing-superpowers/#compile-to-python" id="toc-entry-16"&gt;Compile-to-Python&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/pythons-disappointing-superpowers/#dynamic-test-methods" id="toc-entry-17"&gt;Dynamic test methods&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/pythons-disappointing-superpowers/#morph-into" id="toc-entry-18"&gt;&lt;code class="docutils literal"&gt;morph_into&lt;/code&gt;&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/pythons-disappointing-superpowers/#pytest" id="toc-entry-19"&gt;Pytest&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/pythons-disappointing-superpowers/#assert-rewriting" id="toc-entry-20"&gt;Assert rewriting&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/pythons-disappointing-superpowers/#automatic-dependency-injection-of-fixtures" id="toc-entry-21"&gt;Automatic dependency injection of fixtures&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/pythons-disappointing-superpowers/#others" id="toc-entry-22"&gt;Others&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/pythons-disappointing-superpowers/#conclusion" id="toc-entry-23"&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/pythons-disappointing-superpowers/#links" id="toc-entry-24"&gt;Links&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="gooey"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-1" role="doc-backlink"&gt;Gooey&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/chriskiehl/Gooey"&gt;Gooey&lt;/a&gt; is a library that will re-interpret &lt;a class="reference external" href="https://docs.python.org/3/library/argparse.html"&gt;argparse&lt;/a&gt; entry points as if they were specifying a GUI. In other words, you do “import gooey”, add a decorator and it transforms your CLI program into a GUI program. Apparently it does this by &lt;a class="reference external" href="https://github.com/chriskiehl/Gooey#how-does-it-work"&gt;re-parsing your entry point module&lt;/a&gt;, for reasons I don’t know and don’t need to know. I do know that it works for programs I’ve tried it with, when I wanted to make something that I was using as a CLI, but also needed to be usable by other family members. A pretty cool tool that solves real problems.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="werkzeugs-interactive-debugger"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-2" role="doc-backlink"&gt;Werkzeug’s interactive debugger&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Werkzeug provide a &lt;a class="reference external" href="https://werkzeug.palletsprojects.com/en/2.2.x/debug/"&gt;debugger middleware&lt;/a&gt; which works with any WSGI-compliant Python web framework (which is most of them) with the following extremely useful behaviour:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Crashing errors are automatically intercepted and an error page is shown with a stack trace instead of a generic 500 error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For any and every frame of the stack trace, you can, right from your web browser, start a Python REPL at that frame – i.e. you can effectively continue execution of the crashed program at any point in the stack, or from multiple points simultaneously.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is extremely useful, to say the least.&lt;/p&gt;
&lt;figure class="align-center"&gt;
&lt;img alt="Screenshot of Werkzeug debugger in action" class="full-bleed" src="https://lukeplant.me.uk/blogmedia/werkzeug_debugger_example.png"&gt;
&lt;/figure&gt;
&lt;p&gt;(For Django users – you can use this most easily using &lt;a class="reference external" href="https://django-extensions.readthedocs.io/en/latest/"&gt;django-extensions&lt;/a&gt;)&lt;/p&gt;
&lt;/section&gt;
&lt;section id="hybrid-attributes-in-sqlalchemy"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-3" role="doc-backlink"&gt;Hybrid attributes in SQLAlchemy&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’m sure there are &lt;strong&gt;many&lt;/strong&gt; examples of advanced dynamic techniques in SQLAlchemy, and I’m not the best qualified to talk about them, but here is a cool one I came across that helps explain the kind of thing you can do in Python.&lt;/p&gt;
&lt;p&gt;Suppose you have an ORM object with some attributes that come straight from the database, along with some calculated properties. In the example below we’ve got a model representing an account that might have payments against it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-1" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-2" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-2" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-2"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# DB columns:&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-3" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-3" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;amount_paid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-4" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-4" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;total_purchased&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-5" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-5" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-6" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-6" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-6"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# Calculated properties:&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-7" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-7" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-7"&gt;&lt;/a&gt;    &lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-8" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-8" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-8"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;balance_due&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;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-9" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-9" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-9"&gt;&lt;/a&gt;        &lt;span class="k"&gt;return&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;total_purchased&lt;/span&gt; &lt;span class="o"&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;amount_paid&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-10" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-10" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-10"&gt;&lt;/a&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-11" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-11" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-11"&gt;&lt;/a&gt;    &lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;a id="rest_code_e61eb04c934f4be889be56bf7b0ce439-12" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-12" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-12"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_payment_outstanding&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;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_e61eb04c934f4be889be56bf7b0ce439-13" name="rest_code_e61eb04c934f4be889be56bf7b0ce439-13" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e61eb04c934f4be889be56bf7b0ce439-13"&gt;&lt;/a&gt;        &lt;span class="k"&gt;return&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;balance_due&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Very often you find yourself in a situation like this:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Sometimes you have already loaded an object from the DB, and want to know a calculated value like “does this account have an outstanding payment?”. This shouldn’t execute any more database queries, since you’ve already loaded everything you need to answer that question.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;But sometimes, you want to re-use this logic to do something like “get me all the accounts that have outstanding payments”, and it is vital for efficiency that we do the filtering in the database as a SQL &lt;code class="docutils literal"&gt;WHERE&lt;/code&gt; clause, rather than loading all the records into a Python process and filtering there.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How could we do this in SQLAlchemy &lt;strong&gt;without duplicating the logic&lt;/strong&gt; regarding &lt;code class="docutils literal"&gt;balance_due&lt;/code&gt; and &lt;code class="docutils literal"&gt;has_outstanding_payment&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;The answer is &lt;a class="reference external" href="https://docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html"&gt;hybrid attributes&lt;/a&gt;:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;from sqlalchemy.ext.hybrid import hybrid_property&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;replace &lt;code class="docutils literal"&gt;property&lt;/code&gt; with &lt;code class="docutils literal"&gt;hybrid_property&lt;/code&gt; on the two properties.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;That is all&lt;/strong&gt;. Then you can do:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_eb71c80f90b248d4be98f3a84538de87-1" name="rest_code_eb71c80f90b248d4be98f3a84538de87-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_eb71c80f90b248d4be98f3a84538de87-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_payment_outstanding&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will generate a SQL query that looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code SQL"&gt;&lt;a id="rest_code_9b427bd0d47a4ceda50f4c858877dc7b-1" name="rest_code_9b427bd0d47a4ceda50f4c858877dc7b-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_9b427bd0d47a4ceda50f4c858877dc7b-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_paid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_purchased&lt;/span&gt;
&lt;a id="rest_code_9b427bd0d47a4ceda50f4c858877dc7b-2" name="rest_code_9b427bd0d47a4ceda50f4c858877dc7b-2" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_9b427bd0d47a4ceda50f4c858877dc7b-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;
&lt;a id="rest_code_9b427bd0d47a4ceda50f4c858877dc7b-3" name="rest_code_9b427bd0d47a4ceda50f4c858877dc7b-3" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_9b427bd0d47a4ceda50f4c858877dc7b-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_purchased&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="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_paid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&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="mi"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What’s going on here? If you have a normal model instance &lt;code class="docutils literal"&gt;an_account&lt;/code&gt;, retrieved from a database query, and you do &lt;code class="docutils literal"&gt;an_account.has_payment_outstanding&lt;/code&gt;, then in the &lt;code class="docutils literal"&gt;has_payment_outstanding&lt;/code&gt; function body above, everything is normal: &lt;code class="docutils literal"&gt;self&lt;/code&gt; is bound to &lt;code class="docutils literal"&gt;an_account&lt;/code&gt;, attributes like &lt;code class="docutils literal"&gt;total_purchased&lt;/code&gt; will be &lt;code class="docutils literal"&gt;Decimal&lt;/code&gt; objects that have been loaded from the database.&lt;/p&gt;
&lt;p&gt;However, if you use &lt;code class="docutils literal"&gt;Account.has_payment_outstanding&lt;/code&gt;, the &lt;code class="docutils literal"&gt;self&lt;/code&gt; variable gets bound to a different type of object (the &lt;code class="docutils literal"&gt;Account&lt;/code&gt; class or some proxy), and so things like &lt;code class="docutils literal"&gt;self.total_purchased&lt;/code&gt; instead resolve to objects representing columns/fields. These classes have appropriate “dunder” methods defined, (&lt;code class="docutils literal"&gt;__add__&lt;/code&gt;, &lt;code class="docutils literal"&gt;__gt__&lt;/code&gt; etc) so that operations done on them, such as maths and comparisons, instead of returning values immediately, return new expression objects that track what operations were done. These can then be compiled to SQL later on. So we can execute the filtering as a WHERE clause in the DB.&lt;/p&gt;
&lt;p&gt;The point here is: we are passing both “normal” and “instrumented” types through the same code in order to completely change our execution strategy. This allows us to effectively compile our Python code into SQL on the fly. This is essentially identical to Hillel’s example of passing instrumented objects (“Replacer” class) through normal code to extract certain information about what operations were done.&lt;/p&gt;
&lt;p&gt;This is a very neat feature in SQLAlchemy that I’m rather jealous of as a Django user. If you want the same efficiency in Django, you have to define the instance properties and the database filtering separately, and usually physically not next to each other in the code. The closest we have is &lt;a class="reference external" href="https://docs.djangoproject.com/en/4.1/ref/models/expressions/#query-expressions"&gt;Query expressions&lt;/a&gt; but they don’t work quite the same.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="pony-orm"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-4" role="doc-backlink"&gt;Pony ORM&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This ORM has a way of writing SQL select queries that appears even more magical. Using an example from &lt;a class="reference external" href="https://ponyorm.org/"&gt;their home page&lt;/a&gt;, you write code like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_4420a040a6454eba93cfdf6c41235dad-1" name="rest_code_4420a040a6454eba93cfdf6c41235dad-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_4420a040a6454eba93cfdf6c41235dad-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The result of this is a SQL query that looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code SQL"&gt;&lt;a id="rest_code_3f2f004e6770467f98fe50bc96096b1d-1" name="rest_code_3f2f004e6770467f98fe50bc96096b1d-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_3f2f004e6770467f98fe50bc96096b1d-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ss"&gt;"id"&lt;/span&gt;
&lt;a id="rest_code_3f2f004e6770467f98fe50bc96096b1d-2" name="rest_code_3f2f004e6770467f98fe50bc96096b1d-2" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_3f2f004e6770467f98fe50bc96096b1d-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"customer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"c"&lt;/span&gt;
&lt;a id="rest_code_3f2f004e6770467f98fe50bc96096b1d-3" name="rest_code_3f2f004e6770467f98fe50bc96096b1d-3" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_3f2f004e6770467f98fe50bc96096b1d-3"&gt;&lt;/a&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"order"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"order-1"&lt;/span&gt;
&lt;a id="rest_code_3f2f004e6770467f98fe50bc96096b1d-4" name="rest_code_3f2f004e6770467f98fe50bc96096b1d-4" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_3f2f004e6770467f98fe50bc96096b1d-4"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ss"&gt;"id"&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="ss"&gt;"order-1"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ss"&gt;"customer"&lt;/span&gt;
&lt;a id="rest_code_3f2f004e6770467f98fe50bc96096b1d-5" name="rest_code_3f2f004e6770467f98fe50bc96096b1d-5" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_3f2f004e6770467f98fe50bc96096b1d-5"&gt;&lt;/a&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ss"&gt;"id"&lt;/span&gt;
&lt;a id="rest_code_3f2f004e6770467f98fe50bc96096b1d-6" name="rest_code_3f2f004e6770467f98fe50bc96096b1d-6" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_3f2f004e6770467f98fe50bc96096b1d-6"&gt;&lt;/a&gt;&lt;span class="k"&gt;HAVING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;"order-1"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ss"&gt;"total_price"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A normal understanding of generator expressions suggests that the &lt;code class="docutils literal"&gt;select&lt;/code&gt; function is consuming a generator. But that couldn’t explain the behaviour here. Instead, it actually &lt;a class="reference external" href="https://github.com/ponyorm/pony/blob/27593ffc74184bc334dd301a86fc5f40fdd3ad87/pony/orm/core.py#L5542"&gt;introspects the frame object of the calling code&lt;/a&gt;, then &lt;a class="reference external" href="https://github.com/ponyorm/pony/blob/27593ffc74184bc334dd301a86fc5f40fdd3ad87/pony/orm/decompiling.py#L22"&gt;decompiles the byte code of the generator expression object it finds&lt;/a&gt;, and builds a &lt;a class="reference external" href="https://github.com/ponyorm/pony/blob/27593ffc74184bc334dd301a86fc5f40fdd3ad87/pony/orm/core.py#L5669"&gt;Query&lt;/a&gt; based on the &lt;a class="reference external" href="https://docs.python.org/3/library/ast.html"&gt;AST&lt;/a&gt; objects.&lt;/p&gt;
&lt;p&gt;PonyORM doesn’t advertise all that, of course. It advertises a “beautiful” syntax for writing ORM code, because that’s what matters.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="django"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-5" role="doc-backlink"&gt;Django&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is the web framework I know well, as I used to contribute significantly, and I’ll pick just a few important examples from the ORM, and then from the broader ecosystem.&lt;/p&gt;
&lt;section id="foreignkey"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-6" role="doc-backlink"&gt;ForeignKey&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Suppose, to pick one example of many, you are writing &lt;a class="reference external" href="https://github.com/django-otp/django-otp/"&gt;django-otp&lt;/a&gt;, a third party library that provides a &lt;a class="reference external" href="https://en.wikipedia.org/wiki/One-time_password"&gt;One Time Password&lt;/a&gt; implementation for &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Multi-factor_authentication"&gt;2FA requirements&lt;/a&gt;. You want to create a table of TOTP devices that are linked to user accounts, and so you have something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_d45de0e2aa7b4e9f91205bec591180a5-1" name="rest_code_d45de0e2aa7b4e9f91205bec591180a5-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_d45de0e2aa7b4e9f91205bec591180a5-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TOTPDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_d45de0e2aa7b4e9f91205bec591180a5-2" name="rest_code_d45de0e2aa7b4e9f91205bec591180a5-2" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_d45de0e2aa7b4e9f91205bec591180a5-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth.User'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;related_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'totp_devices'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Later on, you have code that starts with a &lt;code class="docutils literal"&gt;User&lt;/code&gt; object and retrieves their TOTP devices, and it looks something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_a4dfc2dd8fd548dd8c5f240d496815c4-1" name="rest_code_a4dfc2dd8fd548dd8c5f240d496815c4-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_a4dfc2dd8fd548dd8c5f240d496815c4-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;devices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totp_devices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is interesting, because my &lt;code class="docutils literal"&gt;user&lt;/code&gt; variable is an instance of a &lt;code class="docutils literal"&gt;User&lt;/code&gt; model that was provided by core Django, which has no knowledge of the third party project that provides the &lt;code class="docutils literal"&gt;TOTPDevice&lt;/code&gt; model.&lt;/p&gt;
&lt;p&gt;In fact it goes further: I may not be using Django’s &lt;code class="docutils literal"&gt;User&lt;/code&gt; at all, but my own custom &lt;code class="docutils literal"&gt;User&lt;/code&gt; class, and the &lt;code class="docutils literal"&gt;TOTPDevice&lt;/code&gt; model can easily support that too just by doing this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_a16a52a2b38b4684aed5f17744930201-1" name="rest_code_a16a52a2b38b4684aed5f17744930201-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_a16a52a2b38b4684aed5f17744930201-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AUTH_USER_MODEL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"auth.User"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means that my &lt;code class="docutils literal"&gt;User&lt;/code&gt; model has no knowledge of the &lt;code class="docutils literal"&gt;TOTPDevice&lt;/code&gt; class, nor the other way around, yet instances of these classes both get wired up to refer to each other.&lt;/p&gt;
&lt;p&gt;What is actually going on to enable this?&lt;/p&gt;
&lt;p&gt;When you import Django and call &lt;code class="docutils literal"&gt;setup()&lt;/code&gt;, it imports all the apps in your
project. When it comes to the &lt;code class="docutils literal"&gt;TOTPDevice&lt;/code&gt; class, it sees the &lt;code class="docutils literal"&gt;"auth.User"&lt;/code&gt;
reference and finds the class it refers to. It then &lt;strong&gt;modifies that
class&lt;/strong&gt;, adding a &lt;code class="docutils literal"&gt;totp_devices&lt;/code&gt; &lt;a class="reference external" href="https://docs.python.org/3/glossary.html#term-descriptor"&gt;descriptor&lt;/a&gt; object to the class attributes.&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;run-time type modification&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The result is that when you do &lt;code class="docutils literal"&gt;user.totp_devices&lt;/code&gt;, you get a &lt;code class="docutils literal"&gt;Manager&lt;/code&gt; instance that does queries against the &lt;code class="docutils literal"&gt;TOTPDevice&lt;/code&gt; table. It is a specific kind of manager, known as a &lt;code class="docutils literal"&gt;RelatedManager&lt;/code&gt;, with the special property that it automatically does the correct &lt;code class="docutils literal"&gt;filter()&lt;/code&gt; calls to limit returned values to those related to the model instance, among other things.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="relatedmanager"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-7" role="doc-backlink"&gt;RelatedManager&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code class="docutils literal"&gt;RelatedManager&lt;/code&gt; class is interesting in a number of ways. First, it is &lt;a class="reference external" href="https://github.com/django/django/blob/d54717118360e8679aa2bd0c5a1625f3e84712ba/django/db/models/fields/related_descriptors.py#L632"&gt;created as a closure&lt;/a&gt; – meaning the class itself is created inside a function that is called for each relationship. This is &lt;strong&gt;run-time type creation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Second, there are some additional things it needs to support. In Django, projects often override &lt;code class="docutils literal"&gt;Manager&lt;/code&gt; classes, and the related &lt;code class="docutils literal"&gt;QuerySet&lt;/code&gt; classes, to provide a lot of model layer functionality. This is Django’s answer to the “Repository” pattern – popular in some languages, but laborious to create and painful to use &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/evolution-of-a-django-repository-pattern/"&gt;compared to what we have&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Because of this, it’s important that the &lt;code class="docutils literal"&gt;RelatedManager&lt;/code&gt; preserves any custom &lt;code class="docutils literal"&gt;Manager&lt;/code&gt; and &lt;code class="docutils literal"&gt;QuerySet&lt;/code&gt; behaviour defined on the target model. So, the solution is simply that Django makes the created &lt;code class="docutils literal"&gt;RelatedManager&lt;/code&gt; class inherit from your custom &lt;code class="docutils literal"&gt;Manager&lt;/code&gt;. This is &lt;strong&gt;dynamic sub-classing&lt;/strong&gt; – sub-classing of a class that is discovered at run-time. In other OOP languages, you inherit from framework classes. In Python, framework inherits you!&lt;/p&gt;
&lt;/section&gt;
&lt;section id="manytomany-models"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-8" role="doc-backlink"&gt;ManyToMany models&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One common need in database applications is to have &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Many-to-many_(data_model)"&gt;many-to-many relationships&lt;/a&gt; between two models. Typically this can be modelled with a separate table that has foreign keys to the two related tables.&lt;/p&gt;
&lt;p&gt;To make this easy, Django provides a &lt;code class="docutils literal"&gt;ManyToManyField&lt;/code&gt;. For simple cases, it’s
tedious to have to create a model for the intermediate table yourself, so of
course Django just &lt;a class="reference external" href="https://github.com/django/django/blob/d54717118360e8679aa2bd0c5a1625f3e84712ba/django/db/models/fields/related.py#L1279"&gt;creates it for you&lt;/a&gt;
if you don’t provide your own, using &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#type"&gt;type() with 3 arguments&lt;/a&gt;. This is again &lt;strong&gt;run-time type creation&lt;/strong&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="consequences"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-9" role="doc-backlink"&gt;Consequences&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;These examples of run-time type modification or creation are perhaps not the most extreme or mind-bending. But they are something even better: useful. It’s these features, and things like them, that enable an ecosystem of third party Django libraries that can integrate with your own code without any problems.&lt;/p&gt;
&lt;p&gt;Python also always gives us enough flexibility to have a good backwards compatibility story – so that, for example, the swappable User model was introduced with an absolute minimum of fuss for both projects and pluggable Django apps.&lt;/p&gt;
&lt;p&gt;I’m interested in functional programming, Haskell in particular – this blog even ran on Haskell for a time – so I always take interest in developments in the Haskell web framework world. I see lots of cool things, but it always seems that the ecosystems around Haskell web frameworks are at least 10 years behind Django. One key issue is that in contrast to Django or other Python frameworks, Haskell web frameworks almost always have some kind of code generation layer. This can be made to work well for the purposes envisaged by the framework authors, but it never seems to enable the ecosystem of external packages that highly dynamic typing supports.&lt;/p&gt;
&lt;p&gt;Please note that I’m not claiming here that Python is better than Haskell or anything so grand. I’m simply claiming that Python does enable very useful things to be built, and those things are made possible and easy &lt;strong&gt;because of Python’s design, rather than despite it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I think this is important to say. Python has become massively more popular than it was when I first started to use it, and there are increasing numbers of people who use it only because of network effects, and don’t understand why it got so popular in the first place. These people can sometimes assume that it’s fundamentally a poorly designed language that we are just lumped with – today’s PHP – whose best trajectory is to make it more like Java or C# or Rust etc. I think that would be a big mistake.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="baserow"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-10" role="doc-backlink"&gt;Baserow&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One project that takes Python’s run-time type creation further is &lt;a class="reference external" href="https://baserow.io/"&gt;Baserow&lt;/a&gt;. In their case, their customers create database applications, and the metadata for those tables is stored in … tables. They like Django and want to use it as much as possible. But they also want their customers’ actual data tables to be normal RDBMS tables, and therefore benefit from all the typical RDBMS features to make their tables fast and compact etc. (I’ve seen and worked on systems that took the opposite approach, where customer schema was relegated to second class storage – essentially a key-value table – and the performance was predictably awful).&lt;/p&gt;
&lt;p&gt;And they want plug-in authors to be able to use Django too! Some people are just greedy! They &lt;a class="reference external" href="https://baserow.io/blog/how-baserow-lets-users-generate-django-models"&gt;have a nice article describing how they achieved all this&lt;/a&gt;: in short, they use &lt;code class="docutils literal"&gt;type()&lt;/code&gt; for run-time type creation and then leverage everything Django gives them.&lt;/p&gt;
&lt;p&gt;This has the interesting effect that the metadata tables, along with their own business tables, live &lt;strong&gt;at the same level&lt;/strong&gt; as their customers’ tables which are described by those metadata tables. This “meta and non-meta living at the same level” is a neat illustration of what Python’s type system gives you:&lt;/p&gt;
&lt;p&gt;When you first discover the &lt;a class="reference external" href="https://uneezaismail.medium.com/the-circular-mystery-of-pythons-type-and-object-explained-simply-7265c8e49e6c"&gt;mind-bending relationships&lt;/a&gt; around &lt;code class="docutils literal"&gt;type(type) == type&lt;/code&gt;, you might think of an infinitely-recursive relationship. But actually, an infinite relationship has been flattened to being just 3 layers deep – instance, class, metaclass. The last layer just recurses onto itself. The infinity has been tamed, and brought into the same structures that you can already deal with, and without changing language or switching to code generation techniques. This is one reason why many examples of the “hyper-programming” that Hillel talks about can just be dismissed as normal programming – but they are simply hyper-programming that you are now taking for granted.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="cciw-data-retention-policy"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-11" role="doc-backlink"&gt;CCiW data retention policy&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.cciw.co.uk/"&gt;CCiW&lt;/a&gt; is a small charity I’ve been involved with for a long time. When I came to implement its &lt;a class="reference external" href="https://gdpr-info.eu/"&gt;GDPR&lt;/a&gt; and data retention policies, I found another example of how useful it is having access to Django’s meta-layer (generic framework code) on the same level as my normal data layer (business specific classes and tables), in ways that often aren’t the case for statically typed languages that resort to code-generation techniques for some of these things.&lt;/p&gt;
&lt;p&gt;I wanted to have a data retention policy that was both human readable and machine readable, so that:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;We don’t have keep two separate documents in sync.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the CCiW committee and other interested parties would be able to read the  policy that actually gets applied, rather than merely what the policy was supposed to be.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I could have machine level checking of the exhaustiveness of the policy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My solution was to split the data retention policy into two parts:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;a heavily commented, human-and-machine readable &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/master/config/data_retention.yaml"&gt;Literate YAML file&lt;/a&gt; with a &lt;a class="reference external" href="https://www.cciw.co.uk/data-retention-policy/"&gt;nicely formatted version&lt;/a&gt;, that I can genuinely claim &lt;strong&gt;is&lt;/strong&gt; our data retention policy, and that it is automatically applied,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and a &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/master/cciw/data_retention/applying.py"&gt;Python implementation&lt;/a&gt; that reads this file and applies it, along with some additional logic.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A key part of the neatness of this solution is that the generic, higher level code (which reads in a YAML file, and therefore has to treat field names and table names as strings), and the business/domain specific logic can sit right next to each other. The end result is something that’s both efficient and elegant, with great separation of concerns, and virtually self-maintaining – it complains at me automatically if I fail to update it when adding new fields or tables.&lt;/p&gt;
&lt;p&gt;In terms of performance, the daily application for the data retention policy for the entire database requires, at the moment, just 5 UPDATE and 3 DELETE queries, run once a day. This is made possible by:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;using the power of an ORM,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;using generic code to build up &lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt; to &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/37e6d69064c9a5d1372809fa2d723a0e203d21c3/cciw/data_retention/applying.py#L86"&gt;pass&lt;/a&gt; to &lt;a class="reference external" href="https://docs.djangoproject.com/en/4.1/ref/models/querysets/#django.db.models.query.QuerySet.update"&gt;QuerySet.update()&lt;/a&gt;,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;seamlessly integrating these two with &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/37e6d69064c9a5d1372809fa2d723a0e203d21c3/cciw/data_retention/applying.py#L229"&gt;business specific logic&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a class="reference external" href="https://gist.github.com/spookylukey/eeafa220b61e479694e2acf44902b6e1"&gt;Here is one of the queries the ORM generates&lt;/a&gt;, which is complex enough that I wouldn’t attempt to write this by hand, but it correctly applies business logic like not erasing any data of people who still owe us money, and combines all the erasure that needs to be done into a single query.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="query-tracing"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-12" role="doc-backlink"&gt;Query tracing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A common need in database web applications is development tools that monitor what database queries your code is generating and where they are coming from in the code. In Python this is made very easy thanks to &lt;a class="reference external" href="https://docs.python.org/3/library/sys.html?highlight=_getframe#sys._getframe"&gt;sys._getframe&lt;/a&gt; which gives you frame objects of the currently running program.&lt;/p&gt;
&lt;p&gt;For Django, the go-to tool that uses this is &lt;a class="reference external" href="https://github.com/jazzband/django-debug-toolbar"&gt;django-debug-toolbar&lt;/a&gt;, which does an excellent job of pinpointing where queries are coming from.&lt;/p&gt;
&lt;p&gt;There have been times when it has failed me, however. In particular, when you are working with generic code, such as the Django admin or &lt;a class="reference external" href="https://www.django-rest-framework.org/"&gt;Django REST framework&lt;/a&gt;, in which the fields and properties that will be fetched may be defined as strings in declarative code, a stack trace alone is not enough to work out what is triggering the queries. For example, you might have an admin class defined like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9499e8fe60de49cd82ae2b6489f9dab1-1" name="rest_code_9499e8fe60de49cd82ae2b6489f9dab1-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_9499e8fe60de49cd82ae2b6489f9dab1-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyModelAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_9499e8fe60de49cd82ae2b6489f9dab1-2" name="rest_code_9499e8fe60de49cd82ae2b6489f9dab1-2" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_9499e8fe60de49cd82ae2b6489f9dab1-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"field1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"field2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"field3"&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;And the stack trace points you to &lt;a class="reference external" href="https://github.com/django/django/blob/4470c2405c8dbb529501f9d78753e2aa4e9653a2/django/contrib/admin/templatetags/admin_list.py#L212"&gt;this code&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e53a6117c61946c4ab4742c25814fc71-1" name="rest_code_e53a6117c61946c4ab4742c25814fc71-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e53a6117c61946c4ab4742c25814fc71-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_display&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_e53a6117c61946c4ab4742c25814fc71-2" name="rest_code_e53a6117c61946c4ab4742c25814fc71-2" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_e53a6117c61946c4ab4742c25814fc71-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attr&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;lookup_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_admin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It’s correct, but not helpful. I need to know what the value of the local variable &lt;code class="docutils literal"&gt;field_name&lt;/code&gt; is in that loop to work out what is actually causing these queries.&lt;/p&gt;
&lt;p&gt;In addition, in one case I was actually working with DRF endpoints, not the HTML endpoints the debug toolbar is designed for.&lt;/p&gt;
&lt;p&gt;So, I wrote &lt;a class="reference external" href="https://gist.github.com/spookylukey/cafeadfbe776ace223e5520bb0a93652#file-db_debug-py-L313"&gt;my own utilities&lt;/a&gt; that, in addition to extracting the stack, would also include certain local variables for specified functions/methods. I then needed to add some aggregation functionality and pretty-printing for the SQL queries too. Also, I wrote a version of &lt;a class="reference external" href="https://docs.djangoproject.com/en/4.1/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries"&gt;assertNumQueries&lt;/a&gt; that used this better reporting.&lt;/p&gt;
&lt;p&gt;This was highly effective, and enabled me and members of my team to tackle these DRF endpoints that had got entirely out of hand, often taking them from 10,000+ database queries (!) to 10 or 20.&lt;/p&gt;
&lt;p&gt;This is relatively advanced stuff, but not actually all that hard, and it’s within reach of many developers. It doesn’t require learning a whole new language or deep black magic. You can call &lt;code class="docutils literal"&gt;sys._getframe&lt;/code&gt; interactively from a REPL and find out what it does. The biggest hurdle is actually making the mental leap that says “I need to build this, and with Python, I probably can”.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="time-machine-and-pyfakefs"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-13" role="doc-backlink"&gt;time-machine and pyfakefs&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As an example of “entire program transformation”, &lt;a class="reference external" href="https://github.com/adamchainz/time-machine"&gt;time-machine&lt;/a&gt; is an extremely useful library that mocks out date/time functions across your entire program, and &lt;a class="reference external" href="https://github.com/pytest-dev/pyfakefs"&gt;pyfakefs&lt;/a&gt; is one that does the same thing for file-system calls.&lt;/p&gt;
&lt;p&gt;These contrast with libraries like &lt;a class="reference external" href="https://docs.python.org/3/library/unittest.mock.html"&gt;unittest.mock&lt;/a&gt; do that do monkey patching on a more limited, module-by-module basis.&lt;/p&gt;
&lt;p&gt;This technique is primarily useful in automated test suites, but it has a profound impact on the rest of your code base. In other languages, if you want to mock out “all date/time access” or “all filesystem access”, you may end up with a lot of tedious and noisy code to pass these dependencies through layers of code, or complex automatic dependency injection frameworks to avoid that. In Python, those things are rarely necessary, precisely because of things like time-machine and pyfakefs – that is, because your entire program can be manipulated at run-time. Your code base then has the massive benefit of a direct and simple style.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="environment-detection"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-14" role="doc-backlink"&gt;Environment detection&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My current employer is &lt;a class="reference external" href="https://datapane.com/"&gt;Datapane&lt;/a&gt; who make tools for data apps. Many of our customers use &lt;a class="reference external" href="https://jupyter.org/"&gt;Jupyter&lt;/a&gt; or similar environments. To make things work really smoothly, our library codes detects the environment it is running in and responds, and in some cases interacts with this environment (courtesy of &lt;a class="reference external" href="https://datacrayon.com/"&gt;Shahin&lt;/a&gt;, our Jupyter guy). This is an application of Python’s great support for introspection of the running program. There are a bunch of ways you can do this kind of thing:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;checking the system environment in &lt;code class="docutils literal"&gt;os.environ&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checking the contents of &lt;code class="docutils literal"&gt;sys.modules&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;using &lt;code class="docutils literal"&gt;sys._getframe&lt;/code&gt; to examine how you are being called.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;attempting to use &lt;a class="reference external" href="https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.getipython.html#IPython.core.getipython.get_ipython"&gt;get_ipython&lt;/a&gt; and seeing if it works etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is an example of the “whole runtime environment” being dynamic and introspectable, and Jupyter Notebook and its huge ecosystem make great use of this.&lt;/p&gt;
&lt;p&gt;With some of the bigger features we’re working on at the moment at Datapane, we’re needing more advanced ways of adjusting to the running environment. Of course, as long it works, none of the implementation matters to our customers, so we don’t advertise any of that. Our marketing tagline for this is “Jupyter notebook to a shareable data app in 10 seconds”, not “we’re in your Python process, looking at your sys.modules”.&lt;/p&gt;
&lt;p&gt;After doing a grep through my &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;site-packages&lt;/span&gt;&lt;/code&gt;, I found that doing &lt;code class="docutils literal"&gt;sys._getframe&lt;/code&gt; for different kinds of environment detection is relatively common – often used for things like “raise a deprecation warning, but not if we are being called from these specific callees, like our own code”. Here’s just one more example:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://boltons.readthedocs.io/"&gt;boltons&lt;/a&gt; provides a &lt;a class="reference external" href="https://boltons.readthedocs.io/en/latest/typeutils.html?highlight=sentinel#boltons.typeutils.make_sentinel"&gt;make_sentinel&lt;/a&gt; function. The docs state that if you want “pickleability”, the sentinel must be stored in a module-level constant. But the implementation goes further and &lt;a class="reference external" href="https://boltons.readthedocs.io/en/latest/_modules/boltons/typeutils.html#make_sentinel"&gt;checks&lt;/a&gt; you are doing that using a &lt;code class="docutils literal"&gt;sys._getframe&lt;/code&gt; trick. This is just a simple usability enhancement in which code checks that it is being used correctly, made possible by Python’s deep introspection support, but this kind of thing adds up. You will find many similar things in small amounts scattered across different libraries.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="fluent-compiler"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-15" role="doc-backlink"&gt;fluent-compiler&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="https://projectfluent.org/"&gt;Fluent&lt;/a&gt; is a localisation system by Mozilla. I wrote and contributed the initial version of the official &lt;a class="reference external" href="https://github.com/projectfluent/python-fluent"&gt;fluent.runtime&lt;/a&gt; Python implementation, which is an interpreter for the Fluent language, and I also wrote a second implementation, &lt;a class="reference external" href="https://github.com/django-ftl/fluent-compiler"&gt;fluent-compiler&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Of all the libraries I’ve written, this was the one I enjoyed most, and it’s also the least popular it seems – not surprising, since GNU gettext provides a great 90% solution, which is enough for just about everyone, apart from Mozilla and, for some reason I can’t quite remember, me. However, I do know that Mozilla are actually using my second implementation in some of their web projects, via &lt;a class="reference external" href="https://github.com/django-ftl/django-ftl"&gt;django-ftl&lt;/a&gt;, and I’m using it, and it has a few GitHub stars, so that counts as real world!&lt;/p&gt;
&lt;p&gt;Here are some of the Hillel-worthy Python techniques I used:&lt;/p&gt;
&lt;section id="compile-to-python"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-16" role="doc-backlink"&gt;Compile-to-Python&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In fluent-compiler, the implementation strategy I took was to compile the parsed Fluent AST to Python code, and &lt;code class="docutils literal"&gt;exec&lt;/code&gt; it. I actually use Python AST nodes rather than strings, for various security reasons, but this is basically the same as doing &lt;code class="docutils literal"&gt;eval&lt;/code&gt;, and that same technique is used by various other projects like &lt;a class="reference external" href="https://jinja.palletsprojects.com/"&gt;Jinja&lt;/a&gt; and &lt;a class="reference external" href="https://www.makotemplates.org/"&gt;Mako&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If anything qualifies as “programs that create programs on the fly”, then using &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#exec"&gt;exec&lt;/a&gt;, &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#eval"&gt;eval&lt;/a&gt; or &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#compile"&gt;compile&lt;/a&gt; must do so! The main advantage of this technique here is speed. It works particularly well with Fluent, because with a bit of static analysis, we can often completely eliminate the overhead that would otherwise be caused by its more advanced features, like &lt;a class="reference external" href="https://projectfluent.org/fluent/guide/terms.html"&gt;terms and parameterized terms&lt;/a&gt;, so that at run-time they cost us nothing.&lt;/p&gt;
&lt;p&gt;This works even better when combined with PyPy. For the simple and common cases, under CPython 3.11 my benchmarks show a solution using fluent-compiler is about 15% faster than GNU gettext, while under PyPy it’s more than twice as fast.  You should take these numbers with a pinch of salt, but I am confident that the result is not slow, despite having far more advanced capabilities than GNU gettext, which is not true for the first implementation – the compiler is about 5-10x faster than the interpreter for common cases on CPython.&lt;/p&gt;
&lt;p&gt;Additionally, there are some neat tricks you can do when implementing a compiler using the same language that you are compiling to, like &lt;a class="reference external" href="https://github.com/django-ftl/fluent-compiler/blob/6b262af7ce7c5608516aa24aff868ff66f95e0af/src/fluent_compiler/compiler.py#L1366"&gt;evaluating some things ahead of time that you know are constants&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="dynamic-test-methods"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-17" role="doc-backlink"&gt;Dynamic test methods&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;While developing the second implementation, I used the first implementation as a reference. I didn’t want to duplicate every test, or really do anything manually to every test, I just wanted a large sub-set of the test suite to automatically test both implementations. I also wanted failures to clearly indicate which implementation had failed, i.e. I wanted them to run as separate test cases, because the reference implementation could potentially be at fault in some corner cases.&lt;/p&gt;
&lt;p&gt;As I was using unittest, my solution was this: I &lt;a class="reference external" href="https://github.com/django-ftl/fluent-compiler/blob/d1481d61e0bc1a28a228a4b6d5258350d436e765/fluent.runtime/tests/__init__.py#L12"&gt;added a class decorator&lt;/a&gt; that modified the test classes by removing every method that started with &lt;code class="docutils literal"&gt;test_&lt;/code&gt;, replacing it with two methods, one for each implementation.&lt;/p&gt;
&lt;p&gt;This provided almost exactly the same functionality as one of Hillel’s wished-for examples:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Add an output assertion to an optimized function in dev/testing, checking that on all invocations it matches the result of an unoptimized function&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I just used a slightly different technique that better suited my needs, but also made great use of run-time program manipulation.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="morph-into"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-18" role="doc-backlink"&gt;&lt;code class="docutils literal"&gt;morph_into&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As part of the Fluent-to-Python compilation process, I have a tree of AST objects that I want to simplify. Simplifications include things like replacing a “string join” operation that has just one string, with that single string – so we need completely different types of objects. Even in a language that has mutation this can be a bit of a pain, because we’ve got to update the parent object and tell it to replace this child with a different child, and there are many different types of parent object with very different shapes. So my solution was &lt;code class="docutils literal"&gt;morph_into&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-1" name="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-1" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;morph_into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-2" name="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-2" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-3" name="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-3" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-3"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Change `item` into `new_item` without changing its identity&lt;/span&gt;
&lt;a id="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-4" name="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-4" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-4"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-5" name="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-5" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__class__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_item&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_ccf42cebec694cf8afdd7e4265c0d8dd-6" name="rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-6" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#rest_code_ccf42cebec694cf8afdd7e4265c0d8dd-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__dict__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__dict__&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With this solution, we leave the identity of the object the same, so none of the pointers to it need to be updated. But its type and all associated data is changed into something else, so that, other than &lt;a class="reference external" href="https://docs.python.org/3/library/functions.html#id"&gt;id()&lt;/a&gt;, the behaviour of &lt;code class="docutils literal"&gt;item&lt;/code&gt; will now be indistinguishable from &lt;code class="docutils literal"&gt;new_item&lt;/code&gt;. Not many languages allow you to do this!&lt;/p&gt;
&lt;p&gt;I spent quite a lot of time wondering if I should be ashamed or proud of this code. But it turned out there was nothing to be ashamed of – it saved me writing a bunch of code and has had really no downsides.&lt;/p&gt;
&lt;p&gt;Now, this technique won’t work for some things, like builtin primitives, so it can’t be completely generalised. But it doesn’t need that in order to be useful – all the objects I want to do this on are my own custom AST classes that share an interface, so it works and is “type safe” in its own way.&lt;/p&gt;
&lt;p&gt;I’m far from the first person to discover this kind of trick when implementing compilers. In &lt;a class="reference external" href="https://thume.ca/2019/04/29/comparing-compilers-in-rust-haskell-c-and-python/"&gt;this comparison of several groups of people working on a compiler project&lt;/a&gt;, one of the most impressive results came from a single-person team who chose Python. She used way less code, and implemented way more features than the other groups, who all had multiple people on their teams and were using C++/Rust/Haskell etc. Fancy metaprogramming and dynamic typing were a big part of the difference, and by the sounds of it she used exactly the same kinds of things I used:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Another example of the power of metaprogramming and dynamic typing is that we have a 400 line file called &lt;code class="docutils literal"&gt;visit.rs&lt;/code&gt; that is mostly repetitive boilerplate code implementing a visitor on a bunch of AST structures. In Python this could be a short ~10 line function that recursively introspects on the fields of the AST node and visits them (using the &lt;code class="docutils literal"&gt;__dict__&lt;/code&gt; attribute).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Again, I’m not claiming “dynamic typing is better than static typing” – &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/you-cant-compare-language-features-only-languages/"&gt;I don’t think it’s even meaningful to do that comparison&lt;/a&gt;. I’m claiming that highly dynamic meta-programming tricks are indeed a significant part of real Python code, and really do make a big difference.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="pytest"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-19" role="doc-backlink"&gt;Pytest&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Pytest does quite a few dynamic tricks.  Hillel wishes that pytests functionality was more easily usable elsewhere, such as from a REPL. I’ve no doubt this is a legitimate complaint – as it happens, my own use cases involve &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/"&gt;sticking a REPL in my test&lt;/a&gt;, rather than sticking a pytest in my REPL. However, you can’t claim that pytest isn’t a valid example, or isn’t making use of Python’s dynamism – it does, and it provides a lot of useful functionality as a result, including:&lt;/p&gt;
&lt;section id="assert-rewriting"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-20" role="doc-backlink"&gt;Assert rewriting&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The most obvious is perhaps their &lt;a class="reference external" href="https://docs.pytest.org/en/6.2.x/assert.html"&gt;assert rewriting&lt;/a&gt;, which relies on modifying the AST of test modules to inject sub-expression information for when asserts fail. It makes test assertions often much more immediately useful.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="automatic-dependency-injection-of-fixtures"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-21" role="doc-backlink"&gt;Automatic dependency injection of fixtures&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Pytest provides one of the few cases of automatic dependency injection in Python where I’ve thought it was a good idea. It also makes use of Python’s dynamism to make this dependency injection extremely low ceremony. All you need to do is add a parameter to your test function, giving the parameter the name of the &lt;a class="reference external" href="https://docs.pytest.org/en/6.2.x/fixture.html"&gt;fixture&lt;/a&gt; you want, and pytest will find that fixture in its registry and pass it to your function.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="others"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-22" role="doc-backlink"&gt;Others&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post is way too long already, and I’ve done very little actual searching for this stuff – almost all my examples are things that I’ve heard about in the past or done myself, so there must be far more than these in the real world out there. Here are a bunch more I thought of but didn’t have time to expand on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;PyTorch &lt;a class="reference external" href="https://pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html"&gt;automatic differentiation&lt;/a&gt; which uses instrumented objects (similar to the SQLAlchemy example I presume), plus &lt;a class="reference external" href="https://github.com/pytorch/pytorch/blob/master/tools/autograd/derivatives.yaml"&gt;some kind of pattern matching on function calls&lt;/a&gt; that I haven’t had time to investigate.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://vmartin.fr/understanding-automatic-differentiation-in-30-lines-of-python.html"&gt;Understanding Automatic Differentiation in 30 lines of Python&lt;/a&gt; is a great article on how you can build this kind of thing. Crucially, Python’s dynamism makes this kind of thing very accessible to mere mortals.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://vcrpy.readthedocs.io/en/latest/index.html"&gt;VCR.py&lt;/a&gt;: monkey patch all HTTP functions and record interactions, so that the second time we run a test we can use canned responses and avoid the network.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CCiW email tests: &lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/37e6d69064c9a5d1372809fa2d723a0e203d21c3/cciw/utils/tests/base.py#L55"&gt;monkey patch Django’s Atomic decorator and mail sending functions&lt;/a&gt; to ensure we are using “queued email” appropriately inside transactions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lots of tricks in &lt;a class="reference external" href="https://github.com/radiac/django-tagulous"&gt;django-tagulous&lt;/a&gt; to improve usability for developers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://numba.pydata.org/"&gt;numba&lt;/a&gt;: JIT compile and run your Python code on a GPU with a single decorator.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://drf-spectacular.readthedocs.io/en/latest/readme.html"&gt;drf-spectacular&lt;/a&gt;: iterate over all endpoints in a DRF project, introspecting serializers and calling methods with dummy request objects where necessary, to produce an OpenAPI schema.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the stdlib, &lt;a class="reference external" href="https://docs.python.org/3/library/functools.html#functools.total_ordering"&gt;@total_ordering&lt;/a&gt; will look at your class and add missing rich comparison methods.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;depending on an environment flag, &lt;a class="reference external" href="https://github.com/learnscripture/learnscripture.net/blob/3063de7bd364ccf6105b39485830e75b19f902d9/learnscripture/tests/base.py#L232"&gt;automatically wrap all UI test cases in a decorator that takes a screenshot if the test fails&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;EDIT: And some more I discovered after publishing this post, which look interesting:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://github.com/amakelov/mandala"&gt;mandala&lt;/a&gt; - “Computations that save, query and version themselves”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://github.com/google/latexify_py"&gt;latexify&lt;/a&gt; - pretty print Python functions using LaTeX&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://jax.readthedocs.io/en/latest/index.html"&gt;JAX&lt;/a&gt; which has a JIT compiler of Python code and &lt;a class="reference external" href="https://github.com/hips/autograd"&gt;Autograd&lt;/a&gt; which implements automatic differentiation of Python code, possibly similar to PyTorch.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/pythons-disappointing-superpowers/#toc-entry-23" role="doc-backlink"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Why don’t we talk about these much? I think a large part of the answer is that the Python community cares about solving problems, and not about how clever your code is. Clever code, in fact, is looked down on, which is the right attitude – cleverness for the sake of it is always bad. Problem solving is good though. So libraries and projects that do these things don’t tend to brag about their clever techniques, but the problem that they solve.&lt;/p&gt;
&lt;p&gt;Also, many libraries that use these things wrap them up so that you don’t have to know what’s going on – It Just Works. As a newbie, everything about computers is magical and you have to just accept that that’s how they work. Then you take it for granted, and just get on with using it.&lt;/p&gt;
&lt;p&gt;On the other hand, for the implementer, once you understand the magic, it stops being magic, it’s just a feature that the language has.&lt;/p&gt;
&lt;p&gt;Either way, pretty soon none of these things count as “hyper-programming” any more – in one sense, they are just normal Python programming, and that’s the whole point: &lt;strong&gt;Python gives you super powers which are not super powers, they are normal powers&lt;/strong&gt;. Everyone gets to use them, and you don’t need to learn a different language to do so.&lt;/p&gt;
&lt;p&gt;Perhaps we do need to talk about them more, though. At the very least, I hope my examples have sparked some ideas about the kinds of things that are possible in Python.&lt;/p&gt;
&lt;p&gt;Happy hacking!&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/pythons-disappointing-superpowers/#toc-entry-24" 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://lobste.rs/s/9w7ylg/python_s_disappointing_superpowers"&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://twitter.com/spookylukey/status/1620851142849863680"&gt;Discussion of this post on Twitter&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=34611969"&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"/>
  </entry>
  <entry>
    <title>Test factory functions in Django</title>
    <id>https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/</id>
    <updated>2022-11-25T16:07:02Z</updated>
    <published>2022-11-25T16:07:02Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/"/>
    <summary type="html">&lt;p&gt;Patterns for creating model instances in Django project test suites, and some anti-patterns&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;When writing tests for &lt;a class="reference external" href="https://www.djangoproject.com"&gt;Django&lt;/a&gt; projects, you
typically need to create quite a lot of instances of database model objects.
This page documents the patterns I recommend, and the ones I don’t.&lt;/p&gt;
&lt;p&gt;Before I get going, I should mention that a lot of this can be avoided
altogether if you can separate out database independent logic from your models.
But you can only go so far without serious contortions, and you’ll probably
still need to write a fair number of tests that hit the database.&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/test-factory-functions-in-django/#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/test-factory-functions-in-django/#the-aim" id="toc-entry-1"&gt;The aim&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/test-factory-functions-in-django/#custom-factory-functions" id="toc-entry-2"&gt;Custom factory functions&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/test-factory-functions-in-django/#the-auto-sentinel" id="toc-entry-3"&gt;The Auto sentinel&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/test-factory-functions-in-django/#constraints-and-sequences" id="toc-entry-4"&gt;Constraints and sequences&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/test-factory-functions-in-django/#delegation-and-sub-objects" id="toc-entry-5"&gt;Delegation and sub-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/test-factory-functions-in-django/#special-purpose-factories" id="toc-entry-6"&gt;Special purpose factories&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/test-factory-functions-in-django/#sensible-and-minimal-defaults" id="toc-entry-7"&gt;Sensible and minimal defaults&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/test-factory-functions-in-django/#simplified-interface" id="toc-entry-8"&gt;Simplified interface&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/test-factory-functions-in-django/#type-hints" id="toc-entry-9"&gt;Type hints&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/test-factory-functions-in-django/#dont-depend-on-defaults" id="toc-entry-10"&gt;Don’t depend on defaults&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/test-factory-functions-in-django/#enhancements" id="toc-entry-11"&gt;Enhancements&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/test-factory-functions-in-django/#what-not-to-do" id="toc-entry-12"&gt;What not to do&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/test-factory-functions-in-django/#json-yaml-fixtures" id="toc-entry-13"&gt;JSON/YAML fixtures&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/test-factory-functions-in-django/#kwargs" id="toc-entry-14"&gt;&lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt;&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/test-factory-functions-in-django/#django-dynamic-fixture" id="toc-entry-15"&gt;django-dynamic-fixture&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/test-factory-functions-in-django/#factory-boy" id="toc-entry-16"&gt;factory_boy&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/test-factory-functions-in-django/#but-factory-boy-can-also-create-instances-without-saving-them" id="toc-entry-17"&gt;But factory_boy can also create instances without saving them!&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/test-factory-functions-in-django/#but-factory-boy-can-specify-related-data" id="toc-entry-18"&gt;But factory_boy can specify related data!&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/test-factory-functions-in-django/#but-factory-boy-has-faker-integration" id="toc-entry-19"&gt;But factory_boy has faker integration!&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/test-factory-functions-in-django/#but-factory-boy-has-a-create-batch-method" id="toc-entry-20"&gt;But factory_boy has a create_batch method!&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#conclusion" id="toc-entry-21"&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/test-factory-functions-in-django/#footnotes" id="toc-entry-22"&gt;Footnotes&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="the-aim"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-1" role="doc-backlink"&gt;The aim&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We want the following:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Every test should specify each detail about database state it depends on&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The test should not specify any detail it doesn’t depend on&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We should be able to conveniently and succinctly write “what we mean”, without
having to worry about lower level details, especially database schema details
that are not intrinsic to the test.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These things are important so that you can understand tests in isolation, and so
that changes not relevant to a test should not break that test. Otherwise you
will spend a lot of your time fixing broken tests rather than actually doing the
changes you need to do.&lt;/p&gt;
&lt;p&gt;Using Django ORM &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/models/querysets/#create"&gt;create&lt;/a&gt; calls
directly in your tests is not a great solution, because database constraints often
force you to specify fields that you are not interested in.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="custom-factory-functions"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-2" role="doc-backlink"&gt;Custom factory functions&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The answer to this is simply to create your own “factory” functions, with
optional keyword arguments (preferably &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/keyword-only-arguments-in-python/"&gt;keyword only&lt;/a&gt;) for
almost everything. You can add parameters by hand as and when you need them.&lt;/p&gt;
&lt;p&gt;Here are some simple but real examples from the &lt;a class="reference external" href="https://www.cciw.co.uk/"&gt;Christian Camps in Wales&lt;/a&gt; booking system, which has a &lt;code class="docutils literal"&gt;BookingAccount&lt;/code&gt; model
and includes the ability to pay by cheque which is a &lt;code class="docutils literal"&gt;ManualPayment&lt;/code&gt; object:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-1" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_booking_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-2" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-3" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A Booker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-4" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;address_line1&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="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-5" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;address_post_code&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="s2"&gt;"XYZ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-6" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;email&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="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-7" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-7" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-7"&gt;&lt;/a&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BookingAccount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-8" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-8" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-8"&gt;&lt;/a&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BookingAccount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-9" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-9" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-9"&gt;&lt;/a&gt;        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-10" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-10" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-10"&gt;&lt;/a&gt;        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BOOKING_ACCOUNT_EMAIL_SEQUENCE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-11" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-11" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-11"&gt;&lt;/a&gt;        &lt;span class="n"&gt;address_line1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;address_line1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-12" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-12" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-12"&gt;&lt;/a&gt;        &lt;span class="n"&gt;address_post_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;address_post_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-13" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-13" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-13"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-14" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-14" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-14"&gt;&lt;/a&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-15" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-15" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-15"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_manual_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-16" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-16" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-16"&gt;&lt;/a&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-17" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-17" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-17"&gt;&lt;/a&gt;    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BookingAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-18" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-18" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-18"&gt;&lt;/a&gt;    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-19" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-19" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-19"&gt;&lt;/a&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ManualPayment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-20" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-20" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-20"&gt;&lt;/a&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ManualPayment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-21" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-21" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-21"&gt;&lt;/a&gt;        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;create_booking_account&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-22" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-22" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-22"&gt;&lt;/a&gt;        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-23" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-23" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-23"&gt;&lt;/a&gt;        &lt;span class="n"&gt;payment_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ManualPaymentType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CHEQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_bf387bca81714da9b28a4aad1c2acd69-24" name="rest_code_bf387bca81714da9b28a4aad1c2acd69-24" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_bf387bca81714da9b28a4aad1c2acd69-24"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can find the rest of this project’s test factory functions &lt;a class="reference external" href="https://github.com/search?q=%22def+create%22+repo%3Acciw-uk%2Fcciw.co.uk+path%3Afactories.py&amp;amp;type=code&amp;amp;ref=advsearch"&gt;with this search on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A few patterns to note:&lt;/p&gt;
&lt;section id="the-auto-sentinel"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-3" role="doc-backlink"&gt;The Auto sentinel&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A number of places here we used a default value of &lt;code class="docutils literal"&gt;Auto&lt;/code&gt;, which is a custom
object defined as follows:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-1" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_Auto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-2" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-2"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-3" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-3"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Sentinel value indicating an automatic default will be used.&lt;/span&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-4" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-4"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-5" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-6" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-6"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__bool__&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_0c7ff197421c4ecab8a4ee3b52a228a3-7" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-7" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-7"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# Allow `Auto` to be used like `None` or `False` in boolean expressions&lt;/span&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-8" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-8" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-8"&gt;&lt;/a&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-9" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-9" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-9"&gt;&lt;/a&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-10" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-10" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-10"&gt;&lt;/a&gt;
&lt;a id="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-11" name="rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-11" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c7ff197421c4ecab8a4ee3b52a228a3-11"&gt;&lt;/a&gt;&lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_Auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We use &lt;code class="docutils literal"&gt;Auto&lt;/code&gt; instead of &lt;code class="docutils literal"&gt;None&lt;/code&gt; or something else, because:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Sometimes you need to specify &lt;code class="docutils literal"&gt;None&lt;/code&gt; as an actual value (for nullable DB fields), but not want it as a default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Often the correct default needs to be defined dynamically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;you need to create another object at runtime, as in the &lt;code class="docutils literal"&gt;account:
BookingAccount = Auto&lt;/code&gt; line above&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a sensible and correct default depends on some other argument, so requires
some logic in the body of the function.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We create a singleton value &lt;code class="docutils literal"&gt;Auto&lt;/code&gt; so we can do &lt;code class="docutils literal"&gt;if foo is Auto&lt;/code&gt; checks.&lt;/p&gt;
&lt;p&gt;We also give it a type &lt;code class="docutils literal"&gt;Any&lt;/code&gt; so that type checkers don’t complain about using
it as a default value. It doesn’t break type checking for the functions calling
our factory functions.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="constraints-and-sequences"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-4" role="doc-backlink"&gt;Constraints and sequences&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Often you have the problem that a unique constraint on a field makes it
difficult to provide a static default. As in the example above, I’m using a
really simple technique to deal with this – generate a sequence of values that
are unlikely to be specified manually in a test. In the above code, you can see
&lt;code class="docutils literal"&gt;BOOKING_ACCOUNT_EMAIL_SEQUENCE&lt;/code&gt; which is defined like this at the module level:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_8e0df60a2d2545a3aeb442dbf4ccc4b8-1" name="rest_code_8e0df60a2d2545a3aeb442dbf4ccc4b8-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_8e0df60a2d2545a3aeb442dbf4ccc4b8-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;BOOKING_ACCOUNT_EMAIL_SEQUENCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sequence&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;n&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;"booker_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Every time we call &lt;code class="docutils literal"&gt;next()&lt;/code&gt; on this object, we get a distinct value, so we avoid
issues with constraints.&lt;/p&gt;
&lt;p&gt;The &lt;code class="docutils literal"&gt;sequence&lt;/code&gt; utility is actually super simple, but presented here in all
it’s type-hinted glory:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-1" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;itertools&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-2" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-3" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-3"&gt;&lt;/a&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-4" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-4"&gt;&lt;/a&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"T"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-5" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-6" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-7" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-7" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-7"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-8" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-8" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-8"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-9" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-9" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-9"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Generates a sequence of values from a sequence of integers starting at zero,&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-10" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-10" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-10"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    passed through the callable, which must take an integer argument.&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-11" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-11" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-11"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_4f970f89e50b44a5a0b0d33fea12775c-12" name="rest_code_4f970f89e50b44a5a0b0d33fea12775c-12" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_4f970f89e50b44a5a0b0d33fea12775c-12"&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;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You could do something even simpler though – just use a generator expression at
the top level:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_73d1f040971a471a8bebf8fcd169b685-1" name="rest_code_73d1f040971a471a8bebf8fcd169b685-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_73d1f040971a471a8bebf8fcd169b685-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;BOOKING_ACCOUNT_EMAIL_SEQUENCE&lt;/span&gt; &lt;span class="o"&gt;=&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;"booker_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@example.com"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There can be some cases where you need something more complicated than this (for
example to be able to reset sequences) but they are rare in my experience and
fairly easy to write &lt;a class="brackets" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#advanced-sequences" id="footnote-reference-1" role="doc-noteref"&gt;&lt;span class="fn-bracket"&gt;[&lt;/span&gt;1&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="delegation-and-sub-objects"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-5" role="doc-backlink"&gt;Delegation and sub-objects&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Factory functions often delegate to other factory functions, as in the examples
above.&lt;/p&gt;
&lt;p&gt;It’s also quite common to want to specify something about a sub-object. Rather
than build up a tree of objects as the caller, I often add a parameter to the
top-level factory itself. This gives you some independence from the actual
schema.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="special-purpose-factories"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-6" role="doc-backlink"&gt;Special purpose factories&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You aren’t limited to one factory function per model, you can have as many as
you like. For example you might have &lt;code class="docutils literal"&gt;create_staff_user&lt;/code&gt; and
&lt;code class="docutils literal"&gt;create_customer&lt;/code&gt; which take different parameters, but both happen to return
the same &lt;code class="docutils literal"&gt;User&lt;/code&gt; model.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="sensible-and-minimal-defaults"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-7" role="doc-backlink"&gt;Sensible and minimal defaults&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As far as possible, the factory function should pick sensible defaults, based on
what parameters were passed in if any. If it can’t because the caller contradicted themselves, it should raise an exception.&lt;/p&gt;
&lt;p&gt;I normally take the approach that the defaults should produce &lt;strong&gt;minimal&lt;/strong&gt; and
&lt;strong&gt;pristine&lt;/strong&gt; objects, while being &lt;strong&gt;complete&lt;/strong&gt; and &lt;strong&gt;usable&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For example, if your model supports soft-delete via deactivation,
&lt;code class="docutils literal"&gt;active=False&lt;/code&gt; would be a bad default. On the other hand, creating lots of
related objects in order to be “realistic” would not be a good idea.&lt;/p&gt;
&lt;p&gt;You should be pragmatic. For example, for a &lt;code class="docutils literal"&gt;User&lt;/code&gt; object, if a brand new,
“pristine” user is always forced to go through an on-boarding flow on your
website, meaning that every single page but the on-boarding page is blocked
until they complete it, then &lt;code class="docutils literal"&gt;has_onboarded=True&lt;/code&gt; is probably a more sensible
default – only a few of your tests will want &lt;code class="docutils literal"&gt;has_onboarded=False&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In many cases, your main business logic may already have functions that initialise database objects into sensible states when creating them, or when changing their states. Test factory functions will often delegate to them, so that things are set up as close as possible to how they would be normally.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="simplified-interface"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-8" role="doc-backlink"&gt;Simplified interface&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A good factory function will often simplify things for the caller.&lt;/p&gt;
&lt;p&gt;For example, in the CCiW project mentioned, the &lt;code class="docutils literal"&gt;Camp&lt;/code&gt; model has a &lt;code class="docutils literal"&gt;leaders&lt;/code&gt;
relationship, which is a many-to-many. For several good reasons, the leaders are
not &lt;code class="docutils literal"&gt;User&lt;/code&gt; objects, but &lt;code class="docutils literal"&gt;Person&lt;/code&gt; objects, where &lt;code class="docutils literal"&gt;Person&lt;/code&gt; has some metadata
and another many-to-many (!) with &lt;code class="docutils literal"&gt;User&lt;/code&gt; objects. However, when I’m writing a
test, I might want to be able to say something like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_0c84c10221e443c8851a33914f1c7f16-1" name="rest_code_0c84c10221e443c8851a33914f1c7f16-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c84c10221e443c8851a33914f1c7f16-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_0c84c10221e443c8851a33914f1c7f16-2" name="rest_code_0c84c10221e443c8851a33914f1c7f16-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c84c10221e443c8851a33914f1c7f16-2"&gt;&lt;/a&gt;&lt;span class="n"&gt;camp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_camp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;leader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_0c84c10221e443c8851a33914f1c7f16-3" name="rest_code_0c84c10221e443c8851a33914f1c7f16-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_0c84c10221e443c8851a33914f1c7f16-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, I just care that the user is conceptually the leader of the camp. I don’t
care:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;that a camp can have more than one leader&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;that the &lt;code class="docutils literal"&gt;Camp&lt;/code&gt; is actually related to the &lt;code class="docutils literal"&gt;User&lt;/code&gt; object via a &lt;code class="docutils literal"&gt;Person&lt;/code&gt; object.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes I don’t care about specifying who the leader actually is, just that
there is one, so I might want to pass &lt;code class="docutils literal"&gt;leader=True&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My factory function ends up looking like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_b3c7b5b3e48e4186979e44d308aa979b-1" name="rest_code_b3c7b5b3e48e4186979e44d308aa979b-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_b3c7b5b3e48e4186979e44d308aa979b-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_camp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_b3c7b5b3e48e4186979e44d308aa979b-2" name="rest_code_b3c7b5b3e48e4186979e44d308aa979b-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_b3c7b5b3e48e4186979e44d308aa979b-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b3c7b5b3e48e4186979e44d308aa979b-3" name="rest_code_b3c7b5b3e48e4186979e44d308aa979b-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_b3c7b5b3e48e4186979e44d308aa979b-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;leader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b3c7b5b3e48e4186979e44d308aa979b-4" name="rest_code_b3c7b5b3e48e4186979e44d308aa979b-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_b3c7b5b3e48e4186979e44d308aa979b-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;leaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b3c7b5b3e48e4186979e44d308aa979b-5" name="rest_code_b3c7b5b3e48e4186979e44d308aa979b-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_b3c7b5b3e48e4186979e44d308aa979b-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Camp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_b3c7b5b3e48e4186979e44d308aa979b-6" name="rest_code_b3c7b5b3e48e4186979e44d308aa979b-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_b3c7b5b3e48e4186979e44d308aa979b-6"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It’s redundant, but it’s easy to use, and this approach means you isolate many
of your tests from needing changing. Sometimes my factory functions end up
having a &lt;strong&gt;lot&lt;/strong&gt; of parameters, and they’re unlikely to win any beauty contests
— but who really cares? They are easy to understand and modify.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="type-hints"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-9" role="doc-backlink"&gt;Type hints&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Type hints are great for getting good help in your editor when writing tests.
Use them!&lt;/p&gt;
&lt;/section&gt;
&lt;section id="dont-depend-on-defaults"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-10" role="doc-backlink"&gt;Don’t depend on defaults&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If a test requires a certain value, and it happens to be the default that the
factory will use, the test should still specify it. This makes the test more
robust, and allows the factory to change the defaults. If a test doesn’t specify
it, it means it doesn’t care, and it should work with any value the factory
happens to choose.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="enhancements"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-11" role="doc-backlink"&gt;Enhancements&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you are using &lt;a class="reference external" href="https://docs.pytest.org/"&gt;pytest&lt;/a&gt; (which I recommend, along
with &lt;a class="reference external" href="https://pytest-django.readthedocs.io/en/latest/index.html"&gt;pytest-django&lt;/a&gt;), Haki Benita has
nice post that explains how to &lt;a class="reference external" href="https://realpython.com/django-pytest-fixtures/#using-factories-as-fixtures"&gt;use factory functions as pytest fixtures&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="what-not-to-do"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-12" role="doc-backlink"&gt;What not to do&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now for the anti-patterns. If you’re happy with the answer above, you don’t need
to read this bit.&lt;/p&gt;
&lt;section id="json-yaml-fixtures"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-13" role="doc-backlink"&gt;JSON/YAML fixtures&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Django docs used to encourage you to define models in &lt;a class="reference external" href="https://docs.djangoproject.com/en/4.1/howto/initial-data/"&gt;JSON/YAML fixtures&lt;/a&gt; for use in tests.
Don’t do that! &lt;a class="reference external" href="https://youtu.be/ickNQcNXiS4?t=985"&gt;I’ll let Carl Meyer tell you why&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are some legitimate cases for using these kinds of fixtures in tests – in
particular, where you might use the same/similar fixture files for loading data
in a production environment. This is typically when you have essentially static
data that is defined by some external reality, which happens to be stored in a
database table in your app – such as a list of countries and their ISO codes.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="kwargs"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-14" role="doc-backlink"&gt;&lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When writing factory functions, rather than adding loads of parameters, it may
be tempting to just let them accept &lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt; and pass those on to the
underlying model. I usually prefer not to do that, because:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;you get much less help when writing tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you tend to end up overly tied to the actual schema&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="django-dynamic-fixture"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-15" role="doc-backlink"&gt;django-dynamic-fixture&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I used to use &lt;a class="reference external" href="https://github.com/paulocheque/django-dynamic-fixture"&gt;django-dynamic-fixture&lt;/a&gt; to avoid the tedium of
manual factory functions, but have since moved away from that. You are just
introducing a layer between yourself and the code that you actually need to
write, and have to stop it from doing things you don’t want etc. It also doesn’t
understand the “business logic” needed to come up with sensible defaults.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="factory-boy"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-16" role="doc-backlink"&gt;factory_boy&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;OK, &lt;a class="reference external" href="https://factoryboy.readthedocs.io/en/stable/index.html"&gt;factory_boy&lt;/a&gt;,
this is like my comments for django-dynamic-fixture, only more so.&lt;/p&gt;
&lt;p&gt;Let me put it this way:&lt;/p&gt;
&lt;p&gt;You’ve been tasked with providing a &lt;strong&gt;procedure&lt;/strong&gt; for creating model instances,
where that procedure will have sensible defaults, but will allow the caller to
override them. You have to decide what are the appropriate language features of
Python to use. Do you:&lt;/p&gt;
&lt;ol class="upperalpha simple"&gt;
&lt;li&gt;&lt;p&gt;Create a function or a method, with parameters for overriding defaults, or,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Define a new class that inherits from &lt;code class="docutils literal"&gt;Factory&lt;/code&gt;, and use the &lt;strong&gt;body&lt;/strong&gt; of
the class statement to define a procedure?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you chose A), congratulations, you got the right answer! You will be rewarded
for using the language as it was meant to be used, by things like:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;Automatic help inside your editor, both for the parameters and the returned
value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Static type checking if you want it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Everyone being able to modify your code without looking up some documentation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you chose B), you get points for novelty. But you will be punished as follows:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;You will have to invent things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;nested &lt;code class="docutils literal"&gt;class Meta&lt;/code&gt; for &lt;a class="reference external" href="https://factoryboy.readthedocs.io/en/stable/introduction.html#basic-usage"&gt;essential configuration&lt;/a&gt; of &lt;code class="docutils literal"&gt;FactoryOptions&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;nested &lt;code class="docutils literal"&gt;class Params&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;Trait&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;PostGeneration&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;@post_generation&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;LazyAttribute&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;@lazy_attribute&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;@lazy_attribute_sequence&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;LazyFunction&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;SubFactory&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;RelatedFactory&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code class="docutils literal"&gt;SelfAttribute&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://factoryboy.readthedocs.io/en/stable/reference.html#factory.debug"&gt;a debug mode&lt;/a&gt; (of course)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and &lt;a class="reference external" href="https://factoryboy.readthedocs.io/en/stable/orms.html"&gt;much&lt;/a&gt;, &lt;a class="reference external" href="https://factoryboy.readthedocs.io/en/stable/recipes.html"&gt;much&lt;/a&gt; &lt;a class="reference external" href="https://factoryboy.readthedocs.io/en/stable/reference.html"&gt;more&lt;/a&gt;!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You will have to write thousands of lines of code (1700+), thousands more of
tests (5000+), and page after page of documentation (16,000+ words) to support
all this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You will have to get people to read that documentation. Instead of which, they
will spend their evenings writing snarky blog posts complaining about all your
hard work!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You will have an Open Source side project with &lt;a class="reference external" href="https://github.com/FactoryBoy/factory_boy/issues"&gt;hundreds of open issues&lt;/a&gt;, fun!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You will get &lt;strong&gt;less than zero help&lt;/strong&gt; from your editor when using these
factories – not only will it just display &lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt; for inputs, it will
think the output is a &lt;code class="docutils literal"&gt;Factory&lt;/code&gt; instance, which it is not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For people to find what parameters they can pass to a &lt;code class="docutils literal"&gt;Factory&lt;/code&gt;, they will
have to look up the model, &lt;strong&gt;and&lt;/strong&gt; inspect the &lt;code class="docutils literal"&gt;Factory&lt;/code&gt; definition
and decipher its “traits” etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don’t want to add any further to the burden of the authors – they have
suffered enough already! But I do want to deal with a few objections:&lt;/p&gt;
&lt;section id="but-factory-boy-can-also-create-instances-without-saving-them"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-17" role="doc-backlink"&gt;But factory_boy can also create instances without saving them!&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;This is useful if you want to avoid hitting the DB while being able to test a
model method that doesn’t need the DB. In Django, it’s extremely easy to do that
without help, because if you aren’t going to save a model instance, you don’t
need to worry about any attributes other than the ones you specify – models
don’t run validation in the constructor – and so you don’t need factories at
all:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_efe12489097e4b25bb1b775c0858c0e1-1" name="rest_code_efe12489097e4b25bb1b775c0858c0e1-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_efe12489097e4b25bb1b775c0858c0e1-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_address_formatted&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_efe12489097e4b25bb1b775c0858c0e1-2" name="rest_code_efe12489097e4b25bb1b775c0858c0e1-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_efe12489097e4b25bb1b775c0858c0e1-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"123 Main St"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"London"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_efe12489097e4b25bb1b775c0858c0e1-3" name="rest_code_efe12489097e4b25bb1b775c0858c0e1-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_efe12489097e4b25bb1b775c0858c0e1-3"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"123 Main St&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;London&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you really need it, you could always add a &lt;code class="docutils literal"&gt;commit: bool = True&lt;/code&gt; parameter to your factory functions.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="but-factory-boy-can-specify-related-data"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-18" role="doc-backlink"&gt;But factory_boy can specify related data!&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;As is a common pattern in Django, you can use a double underscore in a parameter
to indicate a relationship traversal – from the example in the &lt;a class="reference external" href="https://github.com/FactoryBoy/factory_boy"&gt;README&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9422c09632744f8d8e99f224889749e2-1" name="rest_code_9422c09632744f8d8e99f224889749e2-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_9422c09632744f8d8e99f224889749e2-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9422c09632744f8d8e99f224889749e2-2" name="rest_code_9422c09632744f8d8e99f224889749e2-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_9422c09632744f8d8e99f224889749e2-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9422c09632744f8d8e99f224889749e2-3" name="rest_code_9422c09632744f8d8e99f224889749e2-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_9422c09632744f8d8e99f224889749e2-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'PAID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9422c09632744f8d8e99f224889749e2-4" name="rest_code_9422c09632744f8d8e99f224889749e2-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_9422c09632744f8d8e99f224889749e2-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;customer__is_vip&lt;/span&gt;&lt;span class="o"&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_9422c09632744f8d8e99f224889749e2-5" name="rest_code_9422c09632744f8d8e99f224889749e2-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_9422c09632744f8d8e99f224889749e2-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;address__country&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'AU'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_9422c09632744f8d8e99f224889749e2-6" name="rest_code_9422c09632744f8d8e99f224889749e2-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_9422c09632744f8d8e99f224889749e2-6"&gt;&lt;/a&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is neat, but an anti-pattern in my opinion. As well as specifying that
the order country is Australia, you are also implicitly specifying:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;the Order model stores its address via a foreign key to a separate address model,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;that model has a &lt;code class="docutils literal"&gt;country&lt;/code&gt; field&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and you store country information using ISO-3166 country codes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, you are tying the test more tightly to the schema than you need
to. None of these things are relevant to the test, you just want to specify that
the order is for Australia.&lt;/p&gt;
&lt;p&gt;If instead you do &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;create_order(address_country="AU")&lt;/span&gt;&lt;/code&gt; then you can leave the
factory function to handle the details. That can include normalising a country
code to whatever is the right thing, if it wants to, which is very easy to do
with simple functions that you are in complete control of.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="but-factory-boy-has-faker-integration"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-19" role="doc-backlink"&gt;But factory_boy has faker integration!&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;If you want randomized and realistic looking data, you can use &lt;code class="docutils literal"&gt;faker&lt;/code&gt;
directly with almost exactly the same amount of code:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-1" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;faker&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Faker&lt;/span&gt;
&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-2" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-3" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;faker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Faker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-4" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-5" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-5"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-6" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-6"&gt;&lt;/a&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-7" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-7" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-7"&gt;&lt;/a&gt;        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;faker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;a id="rest_code_1a3f1c0660794c24a35893485f4fc1ff-8" name="rest_code_1a3f1c0660794c24a35893485f4fc1ff-8" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_1a3f1c0660794c24a35893485f4fc1ff-8"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;section id="but-factory-boy-has-a-create-batch-method"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-20" role="doc-backlink"&gt;But factory_boy has a create_batch method!&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;If you need to create a bunch of things, you can just do this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_6b4290ed9c3f4dd296559d3c68cb11a8-1" name="rest_code_6b4290ed9c3f4dd296559d3c68cb11a8-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_6b4290ed9c3f4dd296559d3c68cb11a8-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;create_manual_payment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;which really isn’t very hard, and also means you can have arguments that vary
depending on the loop variable.&lt;/p&gt;
&lt;p&gt;But, because I’m &lt;strong&gt;very&lt;/strong&gt; generous, I will write you a &lt;code class="docutils literal"&gt;create_batch&lt;/code&gt; function
&lt;strong&gt;for free&lt;/strong&gt;. Not only that, I’ll add type hints &lt;strong&gt;for free&lt;/strong&gt;, and I’ll leave it
right here where you can find it, in the public domain:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-1" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-2" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-3" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"T"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-4" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-5" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-6" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-6"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&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;T&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-7" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-7" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-7"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-8" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-8" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-8"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Use `factory` callable to create `count` objects, passing along kwargs&lt;/span&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-9" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-9" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-9"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-10" name="rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-10" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_eb14337bf3174d458d5b8ebe8d02d2ef-10"&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;factory&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;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now you can do the following, and your editor and static type checker will know
exactly what type of objects &lt;code class="docutils literal"&gt;payment_1&lt;/code&gt; and &lt;code class="docutils literal"&gt;payment_2&lt;/code&gt; are:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_245dd30a68d945aa89d7f5130acf26de-1" name="rest_code_245dd30a68d945aa89d7f5130acf26de-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_245dd30a68d945aa89d7f5130acf26de-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;payment_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;create_manual_payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/section&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/test-factory-functions-in-django/#toc-entry-21" role="doc-backlink"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You don’t need to install anything to create factory functions. Just use
built-in language features, and maybe a few tiny helpers like I’ve shown, and
you’re good!&lt;/p&gt;
&lt;p&gt;The only real issue with my approach is that sometimes it can feel a bit tedious
adding another parameter. But slightly tedious code that is extremely easy to
understand and modify, and helps you in all the ways I’ve described, is still a
big win in my book. There will be many days when you long for slightly tedious
code that just works.&lt;/p&gt;
&lt;p&gt;Happy testing!&lt;/p&gt;
&lt;/section&gt;
&lt;hr class="docutils"&gt;
&lt;section id="footnotes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#toc-entry-22" role="doc-backlink"&gt;Footnotes&lt;/a&gt;&lt;/h2&gt;
&lt;aside class="footnote-list brackets"&gt;
&lt;aside class="footnote brackets" id="advanced-sequences" role="doc-footnote"&gt;
&lt;span class="label"&gt;&lt;span class="fn-bracket"&gt;[&lt;/span&gt;&lt;a role="doc-backlink" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#footnote-reference-1"&gt;1&lt;/a&gt;&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;Advanced sequences:&lt;/p&gt;
&lt;p&gt;Sometimes, you might want to reset your sequences, and perhaps automatically
between every test case. I would implement that as follows. Replace the
previous &lt;code class="docutils literal"&gt;sequence&lt;/code&gt; implementation with:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-1" name="rest_code_5276cc4c011e40e38232294b669fe3b1-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-2" name="rest_code_5276cc4c011e40e38232294b669fe3b1-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;itertools&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-3" name="rest_code_5276cc4c011e40e38232294b669fe3b1-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-4" name="rest_code_5276cc4c011e40e38232294b669fe3b1-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-5" name="rest_code_5276cc4c011e40e38232294b669fe3b1-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-6" name="rest_code_5276cc4c011e40e38232294b669fe3b1-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-6"&gt;&lt;/a&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"T"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-7" name="rest_code_5276cc4c011e40e38232294b669fe3b1-7" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-8" name="rest_code_5276cc4c011e40e38232294b669fe3b1-8" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-9" name="rest_code_5276cc4c011e40e38232294b669fe3b1-9" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-9"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-10" name="rest_code_5276cc4c011e40e38232294b669fe3b1-10" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-10"&gt;&lt;/a&gt;    &lt;span class="n"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-11" name="rest_code_5276cc4c011e40e38232294b669fe3b1-11" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-11"&gt;&lt;/a&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-12" name="rest_code_5276cc4c011e40e38232294b669fe3b1-12" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-12"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&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;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-13" name="rest_code_5276cc4c011e40e38232294b669fe3b1-13" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-13"&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;func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-14" name="rest_code_5276cc4c011e40e38232294b669fe3b1-14" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-14"&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;reset_sequence&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-15" name="rest_code_5276cc4c011e40e38232294b669fe3b1-15" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-15"&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;instances&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;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-16" name="rest_code_5276cc4c011e40e38232294b669fe3b1-16" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-16"&gt;&lt;/a&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-17" name="rest_code_5276cc4c011e40e38232294b669fe3b1-17" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-17"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reset_sequence&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_5276cc4c011e40e38232294b669fe3b1-18" name="rest_code_5276cc4c011e40e38232294b669fe3b1-18" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-18"&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;seq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-19" name="rest_code_5276cc4c011e40e38232294b669fe3b1-19" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-19"&gt;&lt;/a&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-20" name="rest_code_5276cc4c011e40e38232294b669fe3b1-20" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-20"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__next__&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;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_5276cc4c011e40e38232294b669fe3b1-21" name="rest_code_5276cc4c011e40e38232294b669fe3b1-21" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_5276cc4c011e40e38232294b669fe3b1-21"&gt;&lt;/a&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;next&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;seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To reset automatically between each test case, assuming use of &lt;code class="docutils literal"&gt;pytest&lt;/code&gt;,
add the following &lt;code class="docutils literal"&gt;autouse&lt;/code&gt; fixture to &lt;code class="docutils literal"&gt;conftest.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-1" name="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-1" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_f4cf1f5a894e4a45941bf3d4879110ae-1"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&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_f4cf1f5a894e4a45941bf3d4879110ae-2" name="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-2" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_f4cf1f5a894e4a45941bf3d4879110ae-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reset_all_sequences&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-3" name="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-3" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_f4cf1f5a894e4a45941bf3d4879110ae-3"&gt;&lt;/a&gt;    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;myproject.factory_utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt;  &lt;span class="c1"&gt;# or wherever&lt;/span&gt;
&lt;a id="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-4" name="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-4" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_f4cf1f5a894e4a45941bf3d4879110ae-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-5" name="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-5" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_f4cf1f5a894e4a45941bf3d4879110ae-5"&gt;&lt;/a&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-6" name="rest_code_f4cf1f5a894e4a45941bf3d4879110ae-6" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/#rest_code_f4cf1f5a894e4a45941bf3d4879110ae-6"&gt;&lt;/a&gt;        &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset_sequence&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/aside&gt;
&lt;/aside&gt;
&lt;/section&gt;</content>
    <category term="django" label="Django"/>
    <category term="python" label="Python"/>
    <category term="python-type-hints" label="Python type hints"/>
  </entry>
  <entry>
    <title>Raising exceptions or returning error objects in Python</title>
    <id>https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/</id>
    <updated>2022-06-06T11:29:35+01:00</updated>
    <published>2022-06-06T11:29:35+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/"/>
    <summary type="html">&lt;p&gt;How returning error objects can provide some advantages over raising exceptions in Python, such as for static type checking tools.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;The other day I got a question about some old code I had written which, instead
of raising an exception for an error condition as the reader expected, returned
an error object:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With your EmailVerifyTokenGenerator class, why do you return error classes
instead of raising custom errors? You could still pass the email to a custom
VerifyExpired exception.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/cciw-uk/cciw.co.uk/blob/eae8005feb95a5383663e69e92d80e11effe5ee6/cciw/bookings/email.py#L41"&gt;https://github.com/cciw-uk/cciw.co.uk/blob/eae8005feb95a5383663e69e92d80e11effe5ee6/cciw/bookings/email.py#L41&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I think I'm too eager to raise errors but maybe there's something I'm missing with classes 😁!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The code in question is below (slightly modified and with several uninteresting
methods removed). It is part of a system for doing email address verification
via magic links in emails.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-1" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-1" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-2" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-2" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-3" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-3" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VerifyFailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-4" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-4" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-4"&gt;&lt;/a&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-5" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-5" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-6" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-6" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-7" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-7" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-7"&gt;&lt;/a&gt;&lt;span class="n"&gt;VerifyFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VerifyFailed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# singleton sentinel value&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-8" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-8" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-9" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-9" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-9"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-10" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-10" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-10"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-11" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-11" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-11"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VerifyExpired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-12" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-12" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-12"&gt;&lt;/a&gt;    &lt;span class="n"&gt;email&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_1f0537cce2be46d3b2bd6261112d5203-13" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-13" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-13"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-14" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-14" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-14"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-15" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-15" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-15"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailVerifyTokenGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-16" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-16" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-16"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;token_for_email&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;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-17" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-17" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-17"&gt;&lt;/a&gt;        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-18" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-18" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-18"&gt;&lt;/a&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-19" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-19" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-19"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;email_from_token&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;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-20" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-20" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-20"&gt;&lt;/a&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-21" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-21" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-21"&gt;&lt;/a&gt;&lt;span class="sd"&gt;        Extracts the verified email address from the token, or a VerifyFailed&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-22" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-22" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-22"&gt;&lt;/a&gt;&lt;span class="sd"&gt;        constant if verification failed, or VerifyExpired if the link expired.&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-23" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-23" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-23"&gt;&lt;/a&gt;&lt;span class="sd"&gt;        """&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-24" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-24" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-24"&gt;&lt;/a&gt;        &lt;span class="n"&gt;max_age&lt;/span&gt; &lt;span class="o"&gt;=&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;EMAIL_VERIFY_TIMEOUT&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-25" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-25" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-25"&gt;&lt;/a&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-26" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-26" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-26"&gt;&lt;/a&gt;            &lt;span class="n"&gt;unencoded_token&lt;/span&gt; &lt;span class="o"&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;url_safe_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-27" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-27" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-27"&gt;&lt;/a&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binascii&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-28" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-28" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-28"&gt;&lt;/a&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;VerifyFailed&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-29" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-29" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-29"&gt;&lt;/a&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-30" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-30" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-30"&gt;&lt;/a&gt;            &lt;span class="k"&gt;return&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;signer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unencoded_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-31" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-31" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-31"&gt;&lt;/a&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SignatureExpired&lt;/span&gt;&lt;span class="p"&gt;,):&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-32" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-32" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-32"&gt;&lt;/a&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;VerifyExpired&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;signer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unencoded_token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-33" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-33" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-33"&gt;&lt;/a&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BadSignature&lt;/span&gt;&lt;span class="p"&gt;,):&lt;/span&gt;
&lt;a id="rest_code_1f0537cce2be46d3b2bd6261112d5203-34" name="rest_code_1f0537cce2be46d3b2bd6261112d5203-34" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_1f0537cce2be46d3b2bd6261112d5203-34"&gt;&lt;/a&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;VerifyFailed&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To sum up, we have a function that extracts an email address from a token,
checking the HMAC signature that it is bundled with. There are 3 possibilities
we want to deal with:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;p&gt;The happy case – we’ve got a valid HMAC code, we just need the email address
returned.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We’ve got an invalid signature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We’ve got a valid but expired signature. We want to handle this separately,
because we’d like to streamline the user experience for getting a new token
generated and sent to them, which means we need to return the email address.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s using &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/topics/signing/"&gt;Django’s signer functions&lt;/a&gt; to do the heavy
lifting, but that doesn’t matter for our purposes, because we are wrapping it
up.&lt;/p&gt;
&lt;p&gt;To get going on designing our API for this bit of code, here are some bad
options:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p&gt;We could have a pair of methods or functions: &lt;code class="docutils literal"&gt;extract_email_from_token&lt;/code&gt;
and &lt;code class="docutils literal"&gt;check_signature&lt;/code&gt;, which can be used independently. This is bad because
you could easily use &lt;code class="docutils literal"&gt;extract_email_from_token&lt;/code&gt; and completely forget to
use &lt;code class="docutils literal"&gt;check_signature&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The principle here is that we want the developer using this API to fall into
&lt;a class="reference external" href="https://blog.codinghorror.com/falling-into-the-pit-of-success/"&gt;the pit of success&lt;/a&gt;. Either
the developer should get their code perfectly correct, or if they don’t, it
either will be obviously broken and not work at all, or at least not subtly
flawed with some nasty bug, like a security issue.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We could have &lt;code class="docutils literal"&gt;email_from_token()&lt;/code&gt; method or function with a return value
of a tuple containing &lt;code class="docutils literal"&gt;(email_address: str, valid_and_not_expired_signature:
bool)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This has a similar issue to above – the calling code could use
&lt;code class="docutils literal"&gt;email_address&lt;/code&gt; and forget to check the validity boolean.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Having ruled those out, we’ve got two main contenders for how to design
&lt;code class="docutils literal"&gt;email_from_token()&lt;/code&gt;:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;p&gt;We could make it raise exceptions for the “invalid” or “expired” cases. We need
to pass extra data for the latter, but we can put it inside the exception
object – as noted by the original questioner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We could make it return error objects for the error cases, as coded above.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Both&lt;/strong&gt; of these satisfy the “pit of success” criterion. If the developer
accidentally does not handle the error cases, they won’t have a bug where we
verified an email address that should not be verified. We will instead probably
have a crasher of some kind, which in the case of a web app, like this one,
means a 500 error page being seen, and something in our logs that makes it
pretty clear what happened.&lt;/p&gt;
&lt;p&gt;If we choose to raise exceptions, naive code which doesn’t check for the
exceptions will simply get no further – the exception will propagate up and
terminate the handler. With the second option where we return error objects,
those objects can’t be accidentally converted into success values – the
&lt;code class="docutils literal"&gt;VerifyExpired&lt;/code&gt; object &lt;strong&gt;contains&lt;/strong&gt; the email address, but it is a completely
different shape of value from the happy case.&lt;/p&gt;
&lt;p&gt;Both of these approaches, to some degree, respect the principle that can be
summed up as &lt;a class="reference external" href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/"&gt;Parse Don’t Validate&lt;/a&gt;. Instead
of merely validating a token and extracting an email address as two independent
things, we are parsing a token, and encoding the result of the validation in the
type of objects that will then flow through the program.&lt;/p&gt;
&lt;p&gt;But which is better?&lt;/p&gt;
&lt;p&gt;One of the influences on my thinking is the way types work in Haskell and other
similar language which make it very easy to create types and constructors. In
Haskell, the following is &lt;strong&gt;all&lt;/strong&gt; the code you need to define a return type for
this kind of function, and the 3 different data constructors you need, which
then do double duty for &lt;a class="reference external" href="https://en.m.wikibooks.org/wiki/Haskell/Pattern_matching"&gt;pattern matching&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code haskell"&gt;&lt;a id="rest_code_a307392e81b94b2f9e6c23ff36d15a27-1" name="rest_code_a307392e81b94b2f9e6c23ff36d15a27-1" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_a307392e81b94b2f9e6c23ff36d15a27-1"&gt;&lt;/a&gt;&lt;span class="kr"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;EmailVerificationResult&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;EmailVerified&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;a id="rest_code_a307392e81b94b2f9e6c23ff36d15a27-2" name="rest_code_a307392e81b94b2f9e6c23ff36d15a27-2" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_a307392e81b94b2f9e6c23ff36d15a27-2"&gt;&lt;/a&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="kt"&gt;VerifyFailed&lt;/span&gt;
&lt;a id="rest_code_a307392e81b94b2f9e6c23ff36d15a27-3" name="rest_code_a307392e81b94b2f9e6c23ff36d15a27-3" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_a307392e81b94b2f9e6c23ff36d15a27-3"&gt;&lt;/a&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="kt"&gt;VerifyExpired&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, Python is not nearly as succinct, but &lt;a class="reference external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt; were a big improvement
for defining things like &lt;code class="docutils literal"&gt;VerifyExpired&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In Haskell, due to static type checking, this pattern makes it pretty much
impossible for the calling code to accidentally fail to handle the return value
correctly. But even in Python, which doesn’t have that built in, I think there
are some compelling advantages:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p&gt;We expect the calling code to handle all the different return values at some
point, and &lt;strong&gt;at the same point&lt;/strong&gt;. (This is unlike some code where we can
raise an exception that we never expect the calling code to specifically
handle – it will be handled by more generic methods at a different layer). It
therefore makes sense that we treat all 3 values as the same kind of thing —
they are just different return values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you instead raise exceptions, you are immediately forcing the calling
code into a special control flow structure, namely the &lt;code class="docutils literal"&gt;try/except&lt;/code&gt; dance,
which can be inconvenient.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In particular, if you want to hand off processing of the value to some other
function or code for handling, you can’t do it easily. For example, code like
this would be fine with the “return error object” method, but significantly
complicated by the “raise exception” method:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_f4287024a9954e9e91826cfe0857849b-1" name="rest_code_f4287024a9954e9e91826cfe0857849b-1" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_f4287024a9954e9e91826cfe0857849b-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;verify_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email_from_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_f4287024a9954e9e91826cfe0857849b-2" name="rest_code_f4287024a9954e9e91826cfe0857849b-2" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_f4287024a9954e9e91826cfe0857849b-2"&gt;&lt;/a&gt;&lt;span class="n"&gt;log_verify_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verify_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_f4287024a9954e9e91826cfe0857849b-3" name="rest_code_f4287024a9954e9e91826cfe0857849b-3" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_f4287024a9954e9e91826cfe0857849b-3"&gt;&lt;/a&gt;&lt;span class="c1"&gt;# etc.&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the years since I wrote the code, however, some perhaps more compelling
arguments have come along for the error object method.&lt;/p&gt;
&lt;p&gt;First, with some small changes (specifically, removing the sentinel singleton
value), we can now add a type signature for &lt;code class="docutils literal"&gt;email_from_token&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_3b073b7e027f4099ba8f016923881175-1" name="rest_code_3b073b7e027f4099ba8f016923881175-1" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_3b073b7e027f4099ba8f016923881175-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;email_from_token&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;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&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;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;VerifyFailed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;VerifyExpired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_3b073b7e027f4099ba8f016923881175-2" name="rest_code_3b073b7e027f4099ba8f016923881175-2" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_3b073b7e027f4099ba8f016923881175-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(You may need &lt;a class="reference external" href="https://docs.python.org/3/library/typing.html#typing.Union"&gt;typing.Union&lt;/a&gt; for older Python
versions)&lt;/p&gt;
&lt;p&gt;This is a benefit in itself from a documentation point of view, and for better
IDE/editor help.&lt;/p&gt;
&lt;p&gt;We can go further with mypy. We can structure our calling code as follows to make
use of &lt;a class="reference external" href="https://hakibenita.com/python-mypy-exhaustive-checking"&gt;mypy exhaustiveness checking&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-1" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-1" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing_extensions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;assert_never&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-2" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-2" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-3" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-3" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;verified_email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmailVerifyTokenGenerator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email_from_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-4" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-4" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-4"&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;verified_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VerifyFailed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-5" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-5" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-5"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-6" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-6" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-6"&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;verified_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VerifyExpired&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-7" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-7" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-7"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-8" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-8" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-8"&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;verified_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-9" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-9" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-9"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-10" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-10" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-10"&gt;&lt;/a&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_094cb84f1dd64230bb82afeec330ba6b-11" name="rest_code_094cb84f1dd64230bb82afeec330ba6b-11" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_094cb84f1dd64230bb82afeec330ba6b-11"&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;verified_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, if we remove one of these blocks, let’s say the &lt;code class="docutils literal"&gt;VerifyExpired&lt;/code&gt; one (or
if we added another option to &lt;code class="docutils literal"&gt;email_from_token&lt;/code&gt;), mypy will catch it for us:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_c9ea70856e734d3da11947378a7bbe13-1" name="rest_code_c9ea70856e734d3da11947378a7bbe13-1" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_c9ea70856e734d3da11947378a7bbe13-1"&gt;&lt;/a&gt;error:&lt;span class="w"&gt; &lt;/span&gt;Argument&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assert_never"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;has&lt;span class="w"&gt; &lt;/span&gt;incompatible&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VerifyExpired"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;expected&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NoReturn"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With the error object method, we could also write our handling code using
&lt;a class="reference external" href="https://peps.python.org/pep-0636/"&gt;structural pattern matching&lt;/a&gt;. The
equivalent code, including our mypy exhaustiveness check, now looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-1" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-1" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;verified_email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmailVerifyTokenGenerator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email_from_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-2" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-2" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;verified_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-3" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-3" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-3"&gt;&lt;/a&gt;    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;VerifyFailed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-4" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-4" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-4"&gt;&lt;/a&gt;        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-5" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-5" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-5"&gt;&lt;/a&gt;    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;VerifyExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expired_token_email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-6" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-6" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-6"&gt;&lt;/a&gt;        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-7" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-7" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-7"&gt;&lt;/a&gt;    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-8" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-8" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-8"&gt;&lt;/a&gt;        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-9" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-9" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-9"&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_8cfa3e1ccb60491fb15d38d2c10716e5-10" name="rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-10" href="https://lukeplant.me.uk/blog/posts/raising-exceptions-or-returning-error-objects-in-python/#rest_code_8cfa3e1ccb60491fb15d38d2c10716e5-10"&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;verified_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This has destructuring of the email address in &lt;code class="docutils literal"&gt;VerifyExpired&lt;/code&gt; built in – it
is bound to the name &lt;code class="docutils literal"&gt;expired_token_email&lt;/code&gt; in that branch.&lt;/p&gt;
&lt;p&gt;Hopefully this gives a good justification for the approach I took with this
code. There are times when exceptions are better – generally when the things
mentioned above don’t apply, or the opposite applies – but I think error objects
also have their place, and sometimes are a much better solution.&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://twitter.com/spookylukey/status/1533831216536997892"&gt;Discussion on Twitter&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="python-type-hints" label="Python type hints"/>
  </entry>
  <entry>
    <title>REPL Python programming and debugging with IPython</title>
    <id>https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/</id>
    <updated>2022-05-04T07:26:56+01:00</updated>
    <published>2022-05-04T07:26:56+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/"/>
    <summary type="html">&lt;p&gt;The flows I use for exploratory programming using a REPL and their advantages.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;When programming in Python, I spend a large amount of time using &lt;a class="reference external" href="https://ipython.org/"&gt;IPython&lt;/a&gt; and its &lt;a class="reference external" href="https://ipython.readthedocs.io/en/stable/"&gt;powerful interactive prompt&lt;/a&gt;, not just for some one-off
calculations, but for significant chunks of actual programming and debugging. I
use it especially for exploratory programming where I’m unsure of the APIs
available to me, or what the state of the system will be at a particular point
in the code.&lt;/p&gt;
&lt;p&gt;While it looks like I’ve been doing this &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/exploratory-programming-with-ipython/"&gt;for 12 years now&lt;/a&gt;, I’m not sure how
widespread this method of working is, as I rarely hear other people talk about
it. So I thought it would be worth sharing in some detail.&lt;/p&gt;
&lt;p&gt;If you like videos and want to see this method in action for writing a test, you
could have a look at the &lt;a class="reference external" href="https://www.youtube.com/watch?v=nEr6T2pL8Es&amp;amp;t=248s"&gt;django-functest video about writing tests
interactively&lt;/a&gt;, or &lt;a class="reference external" href="https://www.youtube.com/watch?v=nEr6T2pL8Es&amp;amp;t=248s"&gt;skip
to the bit where I start using the REPL&lt;/a&gt;.&lt;/p&gt;
&lt;section id="setup"&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;You normally need IPython installed into your current virtualenv for it to work properly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_f4d79de1db23493d9a9018eb00e6938a-1" name="rest_code_f4d79de1db23493d9a9018eb00e6938a-1" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_f4d79de1db23493d9a9018eb00e6938a-1"&gt;&lt;/a&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;ipython
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_53219227496b43ae84a1172c09ffcbab-1" name="rest_code_53219227496b43ae84a1172c09ffcbab-1" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_53219227496b43ae84a1172c09ffcbab-1"&gt;&lt;/a&gt;uv&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;--dev&lt;span class="w"&gt; &lt;/span&gt;ipython
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(See Tips section below if installing IPython is not possible)&lt;/p&gt;
&lt;/section&gt;
&lt;section id="methods"&gt;
&lt;h2&gt;Methods&lt;/h2&gt;
&lt;p&gt;There are basically two ways I open an IPython prompt. The first is by running
it directly from a terminal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code shell"&gt;&lt;a id="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-1" name="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-1" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-1"&gt;&lt;/a&gt;$&lt;span class="w"&gt; &lt;/span&gt;ipython
&lt;a id="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-2" name="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-2" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-2"&gt;&lt;/a&gt;Python&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.9.5&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;default,&lt;span class="w"&gt; &lt;/span&gt;Jul&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2021&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;:45:58&lt;span class="o"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-3" name="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-3" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-3"&gt;&lt;/a&gt;Type&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'copyright'&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'credits'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'license'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;more&lt;span class="w"&gt; &lt;/span&gt;information
&lt;a id="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-4" name="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-4" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-4"&gt;&lt;/a&gt;IPython&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;.3.0&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;An&lt;span class="w"&gt; &lt;/span&gt;enhanced&lt;span class="w"&gt; &lt;/span&gt;Interactive&lt;span class="w"&gt; &lt;/span&gt;Python.&lt;span class="w"&gt; &lt;/span&gt;Type&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'?'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;help.
&lt;a id="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-5" name="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-5" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-6" name="rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-6" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_7121e5fac08d4e2a9465ea2bb6ee31e8-6"&gt;&lt;/a&gt;In&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;:
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In a Django project, &lt;code class="docutils literal"&gt;./manage.py shell&lt;/code&gt; can also be used if you have
IPython installed, with the advantage that it will properly initialise Django
for you.&lt;/p&gt;
&lt;p&gt;This works fine if you want to explore writing some “top level” code – for
example, a new bit of functionality where the entry points have not been created
yet. However, most code I write is not like that. Most of the time I find
myself wanting to write code when I am already 10 levels of function calls
down – for example:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;I’m writing some view code in a Django application, which has a request
object – an object you could not easily recreate if you started from scratch
at an IPython prompt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or, model layer code such as inside a &lt;code class="docutils literal"&gt;save()&lt;/code&gt; method that is itself being
called by some other code you have not written, like the Django admin or some
signal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or, inside a test, where the setup code has already created a whole bunch of
things that are not available to you when you open IPython.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For these cases, I use the second method:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Find the bit of code I want to modify, explore or debug. This will often be my
own code, but could equally be a third party library. I’m always working in a
virtualenv, so even with third party libraries ,“go to definition” in my
editor will take me straight to a writable copy of the code (apart from code
not written in Python).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Insert the code for an IPython prompt and save the file:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_b3694f2ecdc448b2b566ecd2c820db79-1" name="rest_code_b3694f2ecdc448b2b566ecd2c820db79-1" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_b3694f2ecdc448b2b566ecd2c820db79-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;IPython&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;IPython&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I have this bound to a function key in my editor.&lt;/p&gt;
&lt;p&gt;So the code might end up looking like this, if it was a Django view for example:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_afe5a7fa1f0042ac8685063390361612-1" name="rest_code_afe5a7fa1f0042ac8685063390361612-1" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_afe5a7fa1f0042ac8685063390361612-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact_us&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_afe5a7fa1f0042ac8685063390361612-2" name="rest_code_afe5a7fa1f0042ac8685063390361612-2" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_afe5a7fa1f0042ac8685063390361612-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_afe5a7fa1f0042ac8685063390361612-3" name="rest_code_afe5a7fa1f0042ac8685063390361612-3" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_afe5a7fa1f0042ac8685063390361612-3"&gt;&lt;/a&gt;        &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContactUsForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_afe5a7fa1f0042ac8685063390361612-4" name="rest_code_afe5a7fa1f0042ac8685063390361612-4" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_afe5a7fa1f0042ac8685063390361612-4"&gt;&lt;/a&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;a id="rest_code_afe5a7fa1f0042ac8685063390361612-5" name="rest_code_afe5a7fa1f0042ac8685063390361612-5" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_afe5a7fa1f0042ac8685063390361612-5"&gt;&lt;/a&gt;            &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;IPython&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;IPython&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_afe5a7fa1f0042ac8685063390361612-6" name="rest_code_afe5a7fa1f0042ac8685063390361612-6" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_afe5a7fa1f0042ac8685063390361612-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_afe5a7fa1f0042ac8685063390361612-7" name="rest_code_afe5a7fa1f0042ac8685063390361612-7" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_afe5a7fa1f0042ac8685063390361612-7"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I sometimes also might put the snippet inside a new &lt;code class="docutils literal"&gt;if&lt;/code&gt; clause that I
add to catch a particular condition, especially when using this for debugging.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trigger the code in the appropriate way. For the above case, it would involve
first running the Django development server in a terminal, then opening the
web page, filling out the form and pressing submit. For a test, it would be
running the specific test from a terminal. For command line apps it would be
running the app directly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the terminal, I will now find myself in the IPython REPL, and I can go
ahead and:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;work out what code I need to write&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or debug the code that I’m confused about.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that you can write and edit multi-line code at this REPL – it’s not quite as
comfortable as an editor, but it’s OK, and has good history support. There’s
much more to say about IPython and its features that I won’t write here, you can
learn about it in &lt;a class="reference external" href="https://ipython.readthedocs.io/en/stable/"&gt;the docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For those with a background in other languages, it might also be worth pointing
out that a Python REPL is not a different thing from normal Python. Everything
you can do in normal Python, like defining functions and classes, is possible
right there in the REPL.&lt;/p&gt;
&lt;p&gt;Once I’m done with my exploring, I can copy any useful snippets back from the
REPL into my real code, using the history to scan back through what I typed.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="advantages"&gt;
&lt;h2&gt;Advantages&lt;/h2&gt;
&lt;p&gt;The advantages of this method are:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p&gt;You can explore APIs and objects much more easily when you actually have the
object, rather than docs about the object, or what your editor’s
auto-complete tools believe to be true about the object. For example, what
attributes and methods are available on Django’s &lt;a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest"&gt;HttpRequest&lt;/a&gt;?
You don’t have to ensure you’ve got correct type annotations, and hope they
are complete, or make assumptions about what the values are - you’ve got the
object right there, you can inspect it, with extensive and correct tab
completion. You can actually call functions and see what they do.&lt;/p&gt;
&lt;p&gt;For example, Django’s request object typically has a &lt;code class="docutils literal"&gt;user&lt;/code&gt; attribute which
is not part of the &lt;code class="docutils literal"&gt;HttpRequest&lt;/code&gt; definition, because of how it is added
later. It’s visible in a REPL though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can directly explore the state of the system. This can be a huge
advantage for both exploratory programming and debugging.&lt;/p&gt;
&lt;p&gt;For debugging, &lt;a class="reference external" href="https://docs.python.org/3/library/pdb.html"&gt;pdb&lt;/a&gt; and
similar debugging tools and environments will often provide you with “the
state of the system”, and they are much better at being able to step through
multiple layers of code. But I often find that the power and comfort of an
IPython prompt is much nicer for exploring and finding solutions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The feel of this kind of environment is not quite a smooth as &lt;a class="reference external" href="https://mikelevins.github.io/posts/2020-12-18-repl-driven/"&gt;REPL-driven
programming in Lisp&lt;/a&gt;, but I still find
it hugely enjoyable and productive. Compared to many other methods, like
iterating on your code followed by manual or automated testing, it cuts the
latency of the feedback loop from seconds or minutes to milliseconds, and that
is huge.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="tips-and-gotchas"&gt;
&lt;h2&gt;Tips and gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;IPython has tons of cool features that will help you in a REPL environment,
like &lt;a class="reference external" href="https://ipython.org/ipython-doc/3/config/extensions/autoreload.html"&gt;%autoreload&lt;/a&gt;
(thanks &lt;a class="reference external" href="https://twitter.com/be_haki"&gt;haki&lt;/a&gt;), and many other cool &lt;a class="reference external" href="https://ipython.readthedocs.io/en/stable/interactive/magics.html"&gt;magics&lt;/a&gt;. You
should spend the time getting to know them!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In a multi-threaded (or multi-process) environment, IPython prompts won’t play
nice. Turn off multi-threading if possible, or otherwise ensure that you don’t
hit that gotcha.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you do get messed up in a terminal, you may need to manually find the
processes to &lt;a class="reference external" href="https://linuxconfig.org/how-to-kill-a-running-process-on-linux"&gt;kill&lt;/a&gt; and do
&lt;code class="docutils literal"&gt;reset&lt;/code&gt; in your terminal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With the Django development server:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;It’s multi-threaded by default, so either ensure that you don’t hit the view
code multiple times, or use &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--nothreading&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Beware of auto-reloading, which will mess you up if you are still in an
IPython prompt when it kicks in. Either use &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--noreload&lt;/span&gt;&lt;/code&gt; or just ensure
you exit IPython cleanly before doing anything that will trigger a reload.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Beware of environments that capture standard input/output, that will break
this technique.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;pytest captures standard input and breaks things by default. You can turn it
off using &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-s&lt;/span&gt;&lt;/code&gt;. Also if you are using &lt;a class="reference external" href="https://pypi.org/project/pytest-xdist/"&gt;pytest-xdist&lt;/a&gt; you should remember to do &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;-n0&lt;/span&gt;&lt;/code&gt;
to turn off multiple processes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When using &lt;code class="docutils literal"&gt;IPython.embed()&lt;/code&gt; there’s an &lt;a class="reference external" href="https://github.com/ipython/ipython/issues/62"&gt;annoying bug involving closures and
undefined names&lt;/a&gt; due to Python
limitations. It often shows itself when using generator expressions, but at
other times too. It can often be worked around by doing:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_c91f6dbee6da47649833686fc5c78a08-1" name="rest_code_c91f6dbee6da47649833686fc5c78a08-1" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_c91f6dbee6da47649833686fc5c78a08-1"&gt;&lt;/a&gt;&lt;span class="nb"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;locals&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;If for some reason you can’t use IPython, but only have access to the standard
library, the one-liner you need to run a (basic) REPL at any point in your
code is this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9fe269cb2367449590ae1b2463cb36e9-1" name="rest_code_9fe269cb2367449590ae1b2463cb36e9-1" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/#rest_code_9fe269cb2367449590ae1b2463cb36e9-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="end"&gt;
&lt;h2&gt;End&lt;/h2&gt;
&lt;p&gt;That’s it, I hope you found it useful. Do you have any other tips for using this
technique?&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://twitter.com/spookylukey/status/1521776101760057345"&gt;Discussion on Twitter&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</content>
    <category term="django" label="Django"/>
    <category term="ipython" label="IPython"/>
    <category term="python" label="Python"/>
  </entry>
</feed>
