<?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 Python type hints)</title>
  <id>https://lukeplant.me.uk/blog/categories/python-type-hints.xml</id>
  <updated>2025-07-02T17:12:34Z</updated>
  <author>
    <name>Luke Plant</name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://lukeplant.me.uk/blog/categories/python-type-hints.xml"/>
  <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/categories/python-type-hints/"/>
  <generator uri="https://getnikola.com/">Nikola</generator>
  <entry>
    <title>Statically checking Python dicts for completeness</title>
    <id>https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/</id>
    <updated>2025-06-27T11:09:03+01:00</updated>
    <published>2025-06-27T11:09:03+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/"/>
    <summary type="html">&lt;p&gt;A Pythonic way to ensure that your statically-defined dicts are complete, with full source code.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In Python, I often have the situation where I create a dictionary, and want to ensure that it is &lt;strong&gt;complete&lt;/strong&gt; – it has an entry for every valid key.&lt;/p&gt;
&lt;p&gt;Let’s say for my (currently hypothetical) automatic squirrel-deterring water gun system, I have a number of different states the water tank can be in, defined using an enum:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-1" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StrEnum&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-2" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-3" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TankState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StrEnum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-4" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;FULL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FULL"&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-5" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;HALF_FULL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HALF_FULL"&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-6" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;NEARLY_EMPTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NEARLY_EMPTY"&lt;/span&gt;
&lt;a id="rest_code_e3b92173a5f0474b89aa0802cdf31855-7" name="rest_code_e3b92173a5f0474b89aa0802cdf31855-7" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_e3b92173a5f0474b89aa0802cdf31855-7"&gt;&lt;/a&gt;    &lt;span class="n"&gt;EMPTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EMPTY"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In a separate bit of code, I define an RGB colour for each of these states, using a simple dict.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-1" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-2" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x00FF00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-3" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HALF_FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x28D728&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-4" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NEARLY_EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF9900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-5" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_b8cf6f255be5405da20a3f8426f0e320-6" name="rest_code_b8cf6f255be5405da20a3f8426f0e320-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_b8cf6f255be5405da20a3f8426f0e320-6"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is deliberately distinct from my &lt;code class="docutils literal"&gt;TankState&lt;/code&gt; code and related definitions, because it relates to a different part of the project - the user interface. The UI concerns shouldn’t be mixed up with the core logic.&lt;/p&gt;
&lt;p&gt;This dict is fine, and currently complete. But I’d like to ensure that if I add a new item to &lt;code class="docutils literal"&gt;TankState&lt;/code&gt;, I don’t forget to update the &lt;code class="docutils literal"&gt;TANK_STATE_COLORS&lt;/code&gt; dict.&lt;/p&gt;
&lt;p&gt;With a growing ability to do static type checks in Python, &lt;a class="reference external" href="https://stackoverflow.com/questions/72022403/type-hint-for-an-exhaustive-dictionary-with-enum-literal-keys"&gt;some people have asked how we can ensure this using static type checks&lt;/a&gt;. The short answer is, we can’t (at least at the moment).&lt;/p&gt;
&lt;p&gt;But the better question is “how can we (somehow) ensure we don’t forget?” It doesn’t have to be a static type check, as long as it’s very hard to forget, and if it preferably runs as early as possible.&lt;/p&gt;
&lt;p&gt;Instead of shoe-horning everything into static type checks, let’s just make use of the fact that this is Python and we can write any code we want at module level. All we need to do is this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-1" name="rest_code_ebc769b710ff414a828735cf76d69018-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-2" name="rest_code_ebc769b710ff414a828735cf76d69018-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-2"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-3" name="rest_code_ebc769b710ff414a828735cf76d69018-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-3"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-4" name="rest_code_ebc769b710ff414a828735cf76d69018-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_ebc769b710ff414a828735cf76d69018-5" name="rest_code_ebc769b710ff414a828735cf76d69018-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_ebc769b710ff414a828735cf76d69018-5"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"TANK_STATE_COLORS is missing an entry for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s it, that’s the whole technique. I’d argue that this is a pretty much optimal, Pythonic solution to the problem. No clever type tricks to debug later, just 2 lines of plain simple code, and it’s impossible to import your code until you fix the problem, which means you get the early checking you want.
Plus you get &lt;strong&gt;exactly the error message you want&lt;/strong&gt;, not some obscure compiler output, which is also really important.&lt;/p&gt;
&lt;p&gt;It can also be extended if you want to do something more fancy (e.g. allow some values of the enum to be missing), and if it does get in your way, you can turn it off temporarily by just commenting out a couple of lines.&lt;/p&gt;
&lt;section id="thats-not-quite-it"&gt;
&lt;h2&gt;That’s not quite it&lt;/h2&gt;
&lt;p&gt;OK, in a project where I’m using this &lt;strong&gt;a lot&lt;/strong&gt;, I did eventually get bored of this small bit of boilerplate. So, as a Pythonic extension of this Pythonic solution, I now do this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-1" name="rest_code_92e7c9499aba47fbac2310608a002230-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-2" name="rest_code_92e7c9499aba47fbac2310608a002230-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x00FF00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-3" name="rest_code_92e7c9499aba47fbac2310608a002230-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HALF_FULL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x28D728&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-4" name="rest_code_92e7c9499aba47fbac2310608a002230-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NEARLY_EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF9900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-5" name="rest_code_92e7c9499aba47fbac2310608a002230-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;TankState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMPTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0xFF0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-6" name="rest_code_92e7c9499aba47fbac2310608a002230-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-6"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_92e7c9499aba47fbac2310608a002230-7" name="rest_code_92e7c9499aba47fbac2310608a002230-7" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_92e7c9499aba47fbac2310608a002230-7"&gt;&lt;/a&gt;&lt;span class="n"&gt;assert_complete_enumerations_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TANK_STATE_COLORS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Specifically, I’m adding:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;a type hint on the constant&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a call to a clever utility function that does just the right amount of Python magic.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This function needs to be “magical” because we want it to produce good error messages, like we had before. This means it needs to get hold of the name of the dict in the calling module, but functions don’t usually have access to that.&lt;/p&gt;
&lt;p&gt;In addition, it wants to get hold of the type hint (although there would be other ways to infer it without a type hint, there are advantages this way), for which we also need the name.&lt;/p&gt;
&lt;p&gt;The specific magic we need is:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;the clever function needs to get hold of the module that &lt;strong&gt;called it&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it then looks through the module dictionary to get the name of the object that has been passed in&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then it can find type hints, and do the checking.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, because you don’t want to write all that yourself, the code is below. It also supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;having a tuple of &lt;code class="docutils literal"&gt;Enum&lt;/code&gt; types as the key&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;allowing some items to be missing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;using &lt;code class="docutils literal"&gt;Literal&lt;/code&gt; as the key. So you can do things like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_43dffe504c24474bae463e035898536f-1" name="rest_code_43dffe504c24474bae463e035898536f-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-2" name="rest_code_43dffe504c24474bae463e035898536f-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"negative"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-3" name="rest_code_43dffe504c24474bae463e035898536f-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-3"&gt;&lt;/a&gt;    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"zero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-4" name="rest_code_43dffe504c24474bae463e035898536f-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-4"&gt;&lt;/a&gt;    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"positive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-5" name="rest_code_43dffe504c24474bae463e035898536f-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;a id="rest_code_43dffe504c24474bae463e035898536f-6" name="rest_code_43dffe504c24474bae463e035898536f-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_43dffe504c24474bae463e035898536f-6"&gt;&lt;/a&gt;&lt;span class="n"&gt;assert_complete_enumerations_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s got a ton of error checking, because once you get magical then you really don’t want to be debugging obscure messages.&lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;
&lt;p&gt;I hereby place the following code into the public domain - &lt;a class="reference external" href="https://creativecommons.org/publicdomain/zero/1.0/"&gt;CC0 1.0 Universal&lt;/a&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-1" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-1" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-1"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;inspect&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-2" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-2" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-2"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;itertools&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-3" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-3" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-3"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-4" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-4" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-4"&gt;&lt;/a&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-5" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-5" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-5"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-6" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-6" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-6"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-7" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-7" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-8" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-8" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-8"&gt;&lt;/a&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;frozendict&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;frozendict&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-9" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-9" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-9"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-10" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-10" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-10"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-11" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-11" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-11"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_complete_enumerations_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()):&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-12" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-12" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-12"&gt;&lt;/a&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-13" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-13" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-13"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    Magically assert that the dict in the calling module has a&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-14" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-14" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-14"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    value for every item in an enumeration.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-15" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-15" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-15"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    The dict object must be bound to a name in the module.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-16" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-16" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-16"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    It must be type hinted, with the key being an Enum subclass, or Literal.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-17" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-17" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-17"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    The key may also be a tuple of Enum subclasses&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-18" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-18" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-18"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-19" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-19" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-19"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    If you expect some values to be missing, pass them in `allowed_missing`&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-20" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-20" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-20"&gt;&lt;/a&gt;&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-21" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-21" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-21"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt; is not a dict or mapping, it is a &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-22" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-22" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-22"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-23" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-23" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-23"&gt;&lt;/a&gt;    &lt;span class="n"&gt;frame_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_getframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[reportPrivateUsage]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-24" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-24" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-24"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;frame_up&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-25" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-25" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-25"&gt;&lt;/a&gt;    &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getmodule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame_up&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-26" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-26" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-26"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Couldn't get module for frame &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;frame_up&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-27" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-27" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-27"&gt;&lt;/a&gt;    &lt;span class="n"&gt;msg_prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"In module `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`,"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-28" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-28" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-28"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-29" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-29" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-29"&gt;&lt;/a&gt;    &lt;span class="n"&gt;module_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame_up&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;f_locals&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-30" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-30" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-30"&gt;&lt;/a&gt;    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-31" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-31" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-31"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# Find the object:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-32" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-32" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-32"&gt;&lt;/a&gt;    &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;module_dict&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-33" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-33" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-33"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; there is no name for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, please check"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-34" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-34" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-34"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-35" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-35" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-35"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# Any name that has a type hint will do, there will usually be one.&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-36" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-36" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-36"&gt;&lt;/a&gt;    &lt;span class="n"&gt;hints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_type_hints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-37" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-37" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-37"&gt;&lt;/a&gt;    &lt;span class="n"&gt;hinted_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hints&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-38" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-38" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-38"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-39" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-39" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-39"&gt;&lt;/a&gt;        &lt;span class="n"&gt;hinted_names&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-40" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-40" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-40"&gt;&lt;/a&gt;    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; no type hints were found for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, they are needed to use assert_complete_enumerations_dict"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-41" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-41" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-41"&gt;&lt;/a&gt;    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hinted_names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-42" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-42" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-42"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-43" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-43" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-43"&gt;&lt;/a&gt;    &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hints&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-44" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-44" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-44"&gt;&lt;/a&gt;    &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-45" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-45" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-45"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must supply arguments"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-46" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-46" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-46"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-47" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-47" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-47"&gt;&lt;/a&gt;        &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-48" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-48" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-48"&gt;&lt;/a&gt;        &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-49" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-49" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-49"&gt;&lt;/a&gt;        &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-50" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-50" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-50"&gt;&lt;/a&gt;        &lt;span class="n"&gt;frozendict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-51" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-51" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-51"&gt;&lt;/a&gt;    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must be dict/frozendict/Mapping with arguments to use assert_complete_enumerations_dict, not &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-52" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-52" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-52"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-53" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-53" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-53"&gt;&lt;/a&gt;    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-54" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-54" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-54"&gt;&lt;/a&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must have two args"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-55" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-55" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-55"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-56" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-56" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-56"&gt;&lt;/a&gt;    &lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-57" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-57" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-57"&gt;&lt;/a&gt;    &lt;span class="n"&gt;arg0_origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-58" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-58" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-58"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arg0_origin&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-59" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-59" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-59"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# tuple of Enums&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-60" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-60" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-60"&gt;&lt;/a&gt;        &lt;span class="n"&gt;enum_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-61" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-61" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-61"&gt;&lt;/a&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;enum_cls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enum_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-62" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-62" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-62"&gt;&lt;/a&gt;            &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;issubclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-63" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-63" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-63"&gt;&lt;/a&gt;                &lt;span class="n"&gt;enum_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-64" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-64" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-64"&gt;&lt;/a&gt;            &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint must be an Enum to use assert_complete_enumerations_dict, not &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;enum_cls&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-65" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-65" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-65"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-66" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-66" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-66"&gt;&lt;/a&gt;        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enum_cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;enum_cls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enum_list&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-67" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-67" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-67"&gt;&lt;/a&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;arg0_origin&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-68" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-68" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-68"&gt;&lt;/a&gt;        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-69" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-69" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-69"&gt;&lt;/a&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-70" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-70" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-70"&gt;&lt;/a&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;issubclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-71" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-71" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-71"&gt;&lt;/a&gt;            &lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-72" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-72" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-72"&gt;&lt;/a&gt;        &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; type hint must be an Enum to use assert_complete_enumerations_dict, not &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-73" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-73" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-73"&gt;&lt;/a&gt;        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-74" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-74" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-74"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-75" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-75" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-75"&gt;&lt;/a&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-76" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-76" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-76"&gt;&lt;/a&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-77" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-77" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-77"&gt;&lt;/a&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-78" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-78" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-78"&gt;&lt;/a&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-79" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-79" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-79"&gt;&lt;/a&gt;        &lt;span class="c1"&gt;# This is the assert we actually want to do, everything else is just error checking:&lt;/span&gt;
&lt;a id="rest_code_7efcc2725ed74da6a64e5400a05aee4c-80" name="rest_code_7efcc2725ed74da6a64e5400a05aee4c-80" href="https://lukeplant.me.uk/blog/posts/statically-checking-python-dicts-for-completeness/#rest_code_7efcc2725ed74da6a64e5400a05aee4c-80"&gt;&lt;/a&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; needs an entry for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;</content>
    <category term="python" label="Python"/>
    <category term="python-type-hints" label="Python type hints"/>
  </entry>
  <entry>
    <title>Python Type Hints: pyastgrep case study</title>
    <id>https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/</id>
    <updated>2023-10-05T09:45:02Z</updated>
    <published>2023-10-05T09:45:02Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/"/>
    <summary type="html">&lt;p&gt;A second, and more successful attempt to use static type checking in a real Python project&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In a previous post, I did &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study"&gt;a case study on my attempts to add type hints to parsy&lt;/a&gt;. In this post, I’m continuing the series, but in a very different project.&lt;/p&gt;
&lt;p&gt;A while back I forked an existing tool called &lt;a class="reference external" href="https://github.com/hchasestevens/astpath"&gt;astpath&lt;/a&gt; to create my own tool &lt;a class="reference external" href="https://github.com/spookylukey/pyastgrep/"&gt;pyastgrep&lt;/a&gt;, fixing various bugs and usability issues. In the process I rewrote significant parts of the existing code, and added quite a lot of my own. This was a pretty good opportunity for me to attempt to use static typing throughout, since I was not limited by backwards compatibility in the design. Plus it’s a relatively very small amount of code, making it much easier than many of the larger projects I maintain, while still being big enough to be much more than a toy.&lt;/p&gt;
&lt;p&gt;There are at least &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/the-different-uses-of-python-type-hints"&gt;5 different ways that type hints can be used in Python&lt;/a&gt;, but this post focuses on static type checking and interactive programming help. In particular, I wanted to get mypy to catch errors for me, and I was incorporating it in my CI/testing workflows.&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/python-type-hints-pyastgrep-case-study/#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/python-type-hints-pyastgrep-case-study/#about-pyastgrep" id="toc-entry-1"&gt;About pyastgrep&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#things-i-liked" id="toc-entry-2"&gt;Things I liked&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/python-type-hints-pyastgrep-case-study/#issues" id="toc-entry-3"&gt;Issues&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/python-type-hints-pyastgrep-case-study/#mypy-just-isnt-checking-that-code" id="toc-entry-4"&gt;mypy just isn’t checking that code.&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/python-type-hints-pyastgrep-case-study/#iobase-vs-binaryio" id="toc-entry-5"&gt;IOBase vs BinaryIO&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/python-type-hints-pyastgrep-case-study/#missing-types-for-imports" id="toc-entry-6"&gt;Missing types for imports&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/python-type-hints-pyastgrep-case-study/#equality-checks" id="toc-entry-7"&gt;Equality checks&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/python-type-hints-pyastgrep-case-study/#mypy-caching-bugs" id="toc-entry-8"&gt;mypy caching bugs&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/python-type-hints-pyastgrep-case-study/#third-party-types" id="toc-entry-9"&gt;Third party types&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/python-type-hints-pyastgrep-case-study/#duck-typing" id="toc-entry-10"&gt;Duck 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/python-type-hints-pyastgrep-case-study/#false-security" id="toc-entry-11"&gt;False security&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/python-type-hints-pyastgrep-case-study/#exhaustiveness-checking" id="toc-entry-12"&gt;Exhaustiveness checking&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/python-type-hints-pyastgrep-case-study/#decorators" id="toc-entry-13"&gt;Decorators&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/python-type-hints-pyastgrep-case-study/#pyright" id="toc-entry-14"&gt;pyright&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/python-type-hints-pyastgrep-case-study/#summary" id="toc-entry-15"&gt;Summary&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/python-type-hints-pyastgrep-case-study/#links" id="toc-entry-16"&gt;Links&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="about-pyastgrep"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-1" role="doc-backlink"&gt;About pyastgrep&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This tool is a utility to allow you to grep Python code for specific syntax elements using XPath as a powerful query language. The main functions are:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;intelligently work out which files to search (respecting .gitignore files etc.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;parse the Python files as AST and convert to XML&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;apply a user-supplied XPath expression to search for specific AST elements&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;print the results (with the complexity of handling different context strategies and colouring)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is an example showing pyastgrep searching its own code base for all usages of names (variables etc) that contain the substring &lt;code class="docutils literal"&gt;idx&lt;/code&gt;:&lt;/p&gt;
&lt;pre style="background-color: #000; color: #fff;"&gt;
&lt;span&gt;$ pyastgrep './/Name[contains(@id, "idx")]'
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span style=" "&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;60&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;5&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;current_idx&lt;/span&gt;&lt;span&gt; = 0
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;64&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;9&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;linebreak_idx&lt;/span&gt;&lt;span&gt; = python_file_bytes.find(b"\n", current_idx)
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;64&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;55&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       linebreak_idx = python_file_bytes.find(b"\n", &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;current_idx&lt;/span&gt;&lt;span&gt;)
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;65&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;12&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       if &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;linebreak_idx&lt;/span&gt;&lt;span&gt; &amp;lt; 0:
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;66&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;38&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;           line = python_file_bytes[&lt;/span&gt;&lt;span style="color: #f2241f; "&gt;current_idx&lt;/span&gt;&lt;span&gt;:]
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;68&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;38&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;           line = python_file_bytes[&lt;/span&gt;&lt;span style="color: #f2241f; "&gt;current_idx&lt;/span&gt;&lt;span&gt;:linebreak_idx]
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;68&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;50&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;           line = python_file_bytes[current_idx:&lt;/span&gt;&lt;span style="color: #f2241f; "&gt;linebreak_idx&lt;/span&gt;&lt;span&gt;]
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;72&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;12&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       if &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;linebreak_idx&lt;/span&gt;&lt;span&gt; &amp;lt; 0:
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;75&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;13&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;           &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;current_idx&lt;/span&gt;&lt;span&gt; = linebreak_idx + 1
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/files.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;75&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;27&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;           current_idx = &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;linebreak_idx&lt;/span&gt;&lt;span&gt; + 1
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;244&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;9&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;start_line_idx&lt;/span&gt;&lt;span&gt; = line_index - before_context
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;245&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;9&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;end_line_idx&lt;/span&gt;&lt;span&gt; = line_index + after_context
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;246&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;9&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;stop_line_idx&lt;/span&gt;&lt;span&gt; = end_line_idx + 1
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;246&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;25&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       stop_line_idx = &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;end_line_idx&lt;/span&gt;&lt;span&gt; + 1
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;248&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;19&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       if (path, &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;end_line_idx&lt;/span&gt;&lt;span&gt;) in self.printed_context_lines:
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;252&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;19&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       if (path, &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;start_line_idx&lt;/span&gt;&lt;span&gt; - 1) not in self.printed_context_lines:
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;253&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;57&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;           header = self.formatter.format_header(path, &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;start_line_idx&lt;/span&gt;&lt;span&gt;)
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;257&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;44&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       code = "\n".join(result.file_lines[&lt;/span&gt;&lt;span style="color: #f2241f; "&gt;start_line_idx&lt;/span&gt;&lt;span&gt;:stop_line_idx])
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;257&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;59&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       code = "\n".join(result.file_lines[start_line_idx:&lt;/span&gt;&lt;span style="color: #f2241f; "&gt;stop_line_idx&lt;/span&gt;&lt;span&gt;])
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;260&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;13&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       for &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;idx&lt;/span&gt;&lt;span&gt; in range(start_line_idx, stop_line_idx):
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;260&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;26&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       for idx in range(&lt;/span&gt;&lt;span style="color: #f2241f; "&gt;start_line_idx&lt;/span&gt;&lt;span&gt;, stop_line_idx):
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;260&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;42&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;       for idx in range(start_line_idx, &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;stop_line_idx&lt;/span&gt;&lt;span&gt;):
&lt;/span&gt;&lt;span style="color: #eb30ff;  "&gt;src/pyastgrep/printer.py&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #67b11d;  font-weight: bold; "&gt;261&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style="color: #da8b55;  "&gt;51&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;           self.printed_context_lines.add((path, &lt;/span&gt;&lt;span style="color: #f2241f; "&gt;idx&lt;/span&gt;&lt;span&gt;))
&lt;/span&gt;&lt;/pre&gt;&lt;/section&gt;
&lt;section id="things-i-liked"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-2" role="doc-backlink"&gt;Things I liked&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There were some things I really liked about the flow of using a type checker, and I was able to lean on the checker pretty heavily for some things.&lt;/p&gt;
&lt;p&gt;One example of using type-driven programming was going between the layer of my code that found matches and the layer that printed them. I found that the search function needed to go from returning a simple &lt;code class="docutils literal"&gt;Iterable[Match]&lt;/code&gt; eventually all the way to &lt;code class="docutils literal"&gt;Iterable[Match | MissingPath | ReadError | NonElementReturned | FileFinished]&lt;/code&gt;. In each case, I could do something like:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;add the new return value, something like &lt;code class="docutils literal"&gt;yield &lt;span class="pre"&gt;MissingPath(...)&lt;/span&gt;&lt;/code&gt;, in the body of the function.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fix up the function type signature, in response to the type error that mypy would now report.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then respond to the type error that mypy would report in the places that consumed this iterable, usually by implementing handling of the new result type. &lt;code class="docutils literal"&gt;isinstance&lt;/code&gt; checks can be used to drive &lt;a class="reference external" href="https://mypy.readthedocs.io/en/stable/type_narrowing.html"&gt;type narrowing&lt;/a&gt; and satisfy mypy that everything is fine.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was nice when this went through multiple layers, knowing that something was checking it for me and driving me to the next bit of code that needed fixing. Having the types there explicitly also helps to make you conscious of decisions you are making about how the layers are working, which was helpful in keeping the layers straight, so that I can use this code both as a library and a command line tool.&lt;/p&gt;
&lt;p&gt;Being able to convert various exceptions, errors and corner cases to sum types in this way was also great, and I leaned on this heavily — probably more than if I hadn’t been using a type checker. I quite like these kind of ad-hoc sum types – in some ways they often work nicer than sum types in Haskell etc.&lt;/p&gt;
&lt;p&gt;Use of mypy has encouraged me to use lots of small custom classes, because I can use “Find references” to search the code base for everything related to a particular type, or a particular attribute or method. For custom classes I create and use within the code base, this works perfectly, which is really nice. The same goes for renaming things using IDE tools (I’m using Emacs and the pyright LSP server).&lt;/p&gt;
&lt;p&gt;I have a high degree of confidence that I’ll be able to come back to this code base after years without touching and be able to navigate it and make changes very easily.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="issues"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-3" role="doc-backlink"&gt;Issues&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;However, I have a long list of complaints about issues I found too!&lt;/p&gt;
&lt;section id="mypy-just-isnt-checking-that-code"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-4" role="doc-backlink"&gt;mypy just isn’t checking that code.&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You have to turn on at least:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code ini"&gt;&lt;a id="rest_code_c0303bf743a74cd2b686bee7c47411f4-1" name="rest_code_c0303bf743a74cd2b686bee7c47411f4-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_c0303bf743a74cd2b686bee7c47411f4-1"&gt;&lt;/a&gt;&lt;span class="na"&gt;check_untyped_defs&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="s"&gt;true&lt;/span&gt;
&lt;a id="rest_code_c0303bf743a74cd2b686bee7c47411f4-2" name="rest_code_c0303bf743a74cd2b686bee7c47411f4-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_c0303bf743a74cd2b686bee7c47411f4-2"&gt;&lt;/a&gt;&lt;span class="na"&gt;disallow_untyped_calls&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="s"&gt;true&lt;/span&gt;
&lt;a id="rest_code_c0303bf743a74cd2b686bee7c47411f4-3" name="rest_code_c0303bf743a74cd2b686bee7c47411f4-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_c0303bf743a74cd2b686bee7c47411f4-3"&gt;&lt;/a&gt;&lt;span class="na"&gt;disallow_untyped_defs&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="s"&gt;true&lt;/span&gt;
&lt;a id="rest_code_c0303bf743a74cd2b686bee7c47411f4-4" name="rest_code_c0303bf743a74cd2b686bee7c47411f4-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_c0303bf743a74cd2b686bee7c47411f4-4"&gt;&lt;/a&gt;&lt;span class="na"&gt;disallow_incomplete_defs&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="s"&gt;true&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Otherwise you should not be expecting mypy to actually find typing errors. In general, it can be frustrating not knowing whether the lack of red is because you haven’t got errors, or because mypy just isn’t checking code, or can’t meaningfully check anything.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="iobase-vs-binaryio"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-5" role="doc-backlink"&gt;IOBase vs BinaryIO&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;pyastgrep, in common with many similar tools, allows you to process standard input as well as grepping files named on the command line or found by directory walking. So I had to have branches for that, and I had tests for it too.&lt;/p&gt;
&lt;p&gt;I hit a bug regarding encodings, where files not encoded in UTF-8 were causing the tool to crash. I ended up doing an internal change that switched a bunch of types from &lt;code class="docutils literal"&gt;str&lt;/code&gt; to &lt;code class="docutils literal"&gt;bytes&lt;/code&gt;. The code worked, the tests passed, and mypy reported no errors. But later – thankfully before a release – I noticed that I had broken stdin handling.&lt;/p&gt;
&lt;p&gt;It turned out that according to mypy, &lt;code class="docutils literal"&gt;IOBase.read()&lt;/code&gt; returns &lt;code class="docutils literal"&gt;Any&lt;/code&gt;, and not the actual type &lt;code class="docutils literal"&gt;bytes&lt;/code&gt; or &lt;code class="docutils literal"&gt;str&lt;/code&gt;. I had been using &lt;code class="docutils literal"&gt;IOBase&lt;/code&gt; as a type for some of my arguments, which meant that mypy didn’t pick up the problem – if &lt;code class="docutils literal"&gt;Any&lt;/code&gt; appears anywhere, it’s like throwing a “silently disable everything this touches” bomb into the type checker.&lt;/p&gt;
&lt;p&gt;Now, I had been alerted to the problem earlier – mypy thinks that &lt;code class="docutils literal"&gt;sys.stdin&lt;/code&gt; is of type &lt;code class="docutils literal"&gt;typing.TextIO&lt;/code&gt;, not &lt;code class="docutils literal"&gt;IOBase&lt;/code&gt;. However, &lt;code class="docutils literal"&gt;typing.TextIO&lt;/code&gt; is not something you test for at run-time, so it interacts badly with the very useful &lt;code class="docutils literal"&gt;isinstance&lt;/code&gt; type guards and type narrowing. So I had ended up using &lt;code class="docutils literal"&gt;IOBase&lt;/code&gt; as that seemed less problematic.&lt;/p&gt;
&lt;p&gt;In other words, I &lt;strong&gt;had&lt;/strong&gt; added type hints, and I had added &lt;strong&gt;correct&lt;/strong&gt; type hints. But they weren’t correct &lt;strong&gt;enough&lt;/strong&gt;, and therefore turned out to be “wrong”. It was very disappointing that despite the effort I had put in, this kind of type error still got through.&lt;/p&gt;
&lt;p&gt;Fixing the bug involved writing a better, more accurate test that more closely emulated actual stdin handling, and then a very simple change. Fixing my type hints, however, was a much bigger task.&lt;/p&gt;
&lt;p&gt;It involved a long journey of understanding regarding type guards and &lt;a class="reference external" href="https://peps.python.org/pep-0724/"&gt;stricter type guards&lt;/a&gt;, because non-strict type guards (which is what we have at the moment) &lt;a class="reference external" href="https://github.com/python/mypy/issues/13957"&gt;turned out not to work how I thought&lt;/a&gt;. The eventual refactoring now uses the &lt;code class="docutils literal"&gt;typing.BinaryIO&lt;/code&gt; type hint, and some code that seems somewhat fragile in terms of type checking – because there is no way of doing a “negative type guard” for &lt;code class="docutils literal"&gt;BinaryIO&lt;/code&gt;, I have to order my if/elif/else clauses in exactly the right way.&lt;/p&gt;
&lt;p&gt;It’s also closing the barn door after the horse has bolted – I had already fixed the bug, and added much more thorough tests to prove it was fixed. I was hoping that static type checking would have caught this before I had to do that.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="missing-types-for-imports"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-6" role="doc-backlink"&gt;Missing types for imports&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I saw a bug I hadn’t fixed, and one that again I thought mypy might have caught.&lt;/p&gt;
&lt;p&gt;It turns out I had added &lt;code class="docutils literal"&gt;ignore_missing_imports = true&lt;/code&gt; early in development to reduce the errors to a manageable set. This silenced errors relating to lxml and effectively gave me a bunch of &lt;code class="docutils literal"&gt;Any&lt;/code&gt; types floating around instead of something useful.&lt;/p&gt;
&lt;p&gt;Again, this was “my fault”, but I feel it’s fairly typical of what will happen in the real world. Switching to &lt;code class="docutils literal"&gt;ignore_missing_imports = false&lt;/code&gt; can cause so many problems that it will be hard to justify the cost in many cases. In this case, the type stubs it wants me to install require me to “fix” a whole load of static type checking issues relating to lxml that don’t correspond to real run-time issues.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="equality-checks"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-7" role="doc-backlink"&gt;Equality checks&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I switched a bunch of code paths from &lt;code class="docutils literal"&gt;str&lt;/code&gt; to &lt;code class="docutils literal"&gt;Path&lt;/code&gt; at one point. mypy gave me some help, but less than I wanted. For example, this reports no error:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_2d624f6a0c234be194908628aa2d0fea-1" name="rest_code_2d624f6a0c234be194908628aa2d0fea-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_2d624f6a0c234be194908628aa2d0fea-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&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_2d624f6a0c234be194908628aa2d0fea-2" name="rest_code_2d624f6a0c234be194908628aa2d0fea-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_2d624f6a0c234be194908628aa2d0fea-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_2d624f6a0c234be194908628aa2d0fea-3" name="rest_code_2d624f6a0c234be194908628aa2d0fea-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_2d624f6a0c234be194908628aa2d0fea-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&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_2d624f6a0c234be194908628aa2d0fea-4" name="rest_code_2d624f6a0c234be194908628aa2d0fea-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_2d624f6a0c234be194908628aa2d0fea-4"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A comparison between a &lt;code class="docutils literal"&gt;str&lt;/code&gt; and a &lt;code class="docutils literal"&gt;Path&lt;/code&gt; always returns &lt;code class="docutils literal"&gt;False&lt;/code&gt;, so it’s not a useful thing to do, and therefore a developer error. I meant to do the comparison to &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;"-"&lt;/span&gt;&lt;/code&gt; before I converted the input &lt;code class="docutils literal"&gt;str&lt;/code&gt; objects to internal &lt;code class="docutils literal"&gt;Path&lt;/code&gt; objects. It’s conceptually a &lt;code class="docutils literal"&gt;TypeError&lt;/code&gt;, but not actually one. Thankfully I had tests that failed.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="mypy-caching-bugs"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-8" role="doc-backlink"&gt;mypy caching bugs&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Several times I had to blow away &lt;code class="docutils literal"&gt;.mypy_cache&lt;/code&gt; to get errors to appear. This is not a fundamental problem with the idea of static typing, but it makes very big difference to the whole flow of leaning on mypy. I often noticed only when I &lt;strong&gt;knew&lt;/strong&gt; that mypy should be reporting errors due to a change I just made, but it wasn’t – I have no idea how many other times it was happening. When interpreting “mypy reports no errors”, there are now about half a dozen reasons why that might be the case, only one of which is “you fixed everything and your code is correct”.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="third-party-types"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-9" role="doc-backlink"&gt;Third party types&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Are types provided by third party libs or typeshed reliable?&lt;/p&gt;
&lt;p&gt;No, they are not. For example, I discovered this one in &lt;a class="reference external" href="https://github.com/python/typeshed/blob/a094aa09c2aa47721664d3fdef91eda4fac24ebb/stdlib/_ast.pyi#L19"&gt;typeshed/stdlib/_ast.pyi&lt;/a&gt; among many others:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_a2d6163fd16242be876315d6163fc1d2-1" name="rest_code_a2d6163fd16242be876315d6163fc1d2-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_a2d6163fd16242be876315d6163fc1d2-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_a2d6163fd16242be876315d6163fc1d2-2" name="rest_code_a2d6163fd16242be876315d6163fc1d2-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_a2d6163fd16242be876315d6163fc1d2-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_a2d6163fd16242be876315d6163fc1d2-3" name="rest_code_a2d6163fd16242be876315d6163fc1d2-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_a2d6163fd16242be876315d6163fc1d2-3"&gt;&lt;/a&gt;    &lt;span class="c1"&gt;# TODO: Not all nodes have all of the following attributes&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is probably not typeshed’s “fault” — it’s a problem trying to retro-fit static types to a language and stdlib that wasn’t designed for them.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="duck-typing"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-10" role="doc-backlink"&gt;Duck typing&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As soon as you want to use duck typing, which I did want to, you’ve got more work ahead of you, work that isn’t really to do with solving your actual problem. There are solutions such as &lt;a class="reference external" href="https://docs.python.org/3/library/typing.html#typing.Protocol"&gt;Protocol&lt;/a&gt;, but I’m simply noting you do have significant amounts of extra work for the type checker to understand idiomatic Python.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="false-security"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-11" role="doc-backlink"&gt;False security&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I fairly often got that sense of “it type checks, and everything works first time I run it, cool!”&lt;/p&gt;
&lt;p&gt;Sometimes, it was an illusion though – take this code:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_48ce78cb02d843aca6af9d75f08cc041-1" name="rest_code_48ce78cb02d843aca6af9d75f08cc041-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_48ce78cb02d843aca6af9d75f08cc041-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;UseColor&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_48ce78cb02d843aca6af9d75f08cc041-2" name="rest_code_48ce78cb02d843aca6af9d75f08cc041-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_48ce78cb02d843aca6af9d75f08cc041-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;colorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_default_colorer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;a id="rest_code_48ce78cb02d843aca6af9d75f08cc041-3" name="rest_code_48ce78cb02d843aca6af9d75f08cc041-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_48ce78cb02d843aca6af9d75f08cc041-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;UseColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NEVER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_48ce78cb02d843aca6af9d75f08cc041-4" name="rest_code_48ce78cb02d843aca6af9d75f08cc041-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_48ce78cb02d843aca6af9d75f08cc041-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;colorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NullColorer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I had typed &lt;code class="docutils literal"&gt;colors&lt;/code&gt; instead of the correct &lt;code class="docutils literal"&gt;color&lt;/code&gt; in the second branch, but I got no squiggly red lines. This was because of a lurking &lt;code class="docutils literal"&gt;Any&lt;/code&gt; – the argparse &lt;code class="docutils literal"&gt;args&lt;/code&gt; object is actually an &lt;code class="docutils literal"&gt;Any&lt;/code&gt;. This tripped me up, because I didn’t have any tests for that line of code.&lt;/p&gt;
&lt;p&gt;Having &lt;code class="docutils literal"&gt;strict = true&lt;/code&gt; in your mypy config doesn’t fix this. I think I’d need a way to say “warn me for about anywhere that &lt;code class="docutils literal"&gt;Any&lt;/code&gt; leaks into my code base”, but even if it existed I imagine I would not like it.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="exhaustiveness-checking"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-12" role="doc-backlink"&gt;Exhaustiveness checking&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;mypy fails to find the obvious issue in this bit of code:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-1" name="rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-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;-&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_e9d194801ebe43fa8c0ea4d20e1f94cc-2" name="rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-3" name="rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-3"&gt;&lt;/a&gt;        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt;
&lt;a id="rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-4" name="rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_e9d194801ebe43fa8c0ea4d20e1f94cc-4"&gt;&lt;/a&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I hoped I’d at least get a warning for a potential unbound variable. This comes up with cases where you want exhaustiveness checking, like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_aaebb752a1644177aae62501771f5583-1" name="rest_code_aaebb752a1644177aae62501771f5583-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_aaebb752a1644177aae62501771f5583-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;print_greeting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Greeting&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_aaebb752a1644177aae62501771f5583-2" name="rest_code_aaebb752a1644177aae62501771f5583-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_aaebb752a1644177aae62501771f5583-2"&gt;&lt;/a&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Greeting&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HELLO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_aaebb752a1644177aae62501771f5583-3" name="rest_code_aaebb752a1644177aae62501771f5583-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_aaebb752a1644177aae62501771f5583-3"&gt;&lt;/a&gt;        &lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt;
&lt;a id="rest_code_aaebb752a1644177aae62501771f5583-4" name="rest_code_aaebb752a1644177aae62501771f5583-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_aaebb752a1644177aae62501771f5583-4"&gt;&lt;/a&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Greeting&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOODBYE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_aaebb752a1644177aae62501771f5583-5" name="rest_code_aaebb752a1644177aae62501771f5583-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_aaebb752a1644177aae62501771f5583-5"&gt;&lt;/a&gt;        &lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"goodbye"&lt;/span&gt;
&lt;a id="rest_code_aaebb752a1644177aae62501771f5583-6" name="rest_code_aaebb752a1644177aae62501771f5583-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_aaebb752a1644177aae62501771f5583-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_aaebb752a1644177aae62501771f5583-7" name="rest_code_aaebb752a1644177aae62501771f5583-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_aaebb752a1644177aae62501771f5583-7"&gt;&lt;/a&gt;    &lt;span class="nb"&gt;print&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;greeting&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;username&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;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can get this right using an &lt;code class="docutils literal"&gt;else&lt;/code&gt; branch with &lt;a class="reference external" href="https://typing.readthedocs.io/en/latest/source/unreachable.html"&gt;assert_never&lt;/a&gt;, but it’s annoying that you have to remember to do this.&lt;/p&gt;
&lt;p&gt;[Update 2024: while mypy doesn’t see the potential unbound variable error, even with &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;--strict&lt;/span&gt;&lt;/code&gt;, I’ve found pyright does spot it, and these days I’m using pyright more and more]&lt;/p&gt;
&lt;/section&gt;
&lt;section id="decorators"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-13" role="doc-backlink"&gt;Decorators&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="reference external" href="https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators"&gt;Type hints for decorators&lt;/a&gt; are … bad. If you want parameterised decorators, or other people’s decorators that don’t have types&lt;/p&gt;
&lt;p&gt;[Apologies for the unfinished sentence above. I don’t want to risk a repeated head-against-table moment that the first attempt triggered, once was painful enough]&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="pyright"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-14" role="doc-backlink"&gt;pyright&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;More recently I’ve tried pyright as an alternative to mypy. Generally I’ve found it to be significantly less buggy. However, mypy has a lot going for it in terms of features and extensions, and I don’t really want to have two different type checkers. At the moment I’m experimenting with mainly using pyright for interactive checks in my editor, and using mypy for pre-commit/CI checks.&lt;/p&gt;
&lt;p&gt;The overlapping feature sets can be kind of annoying though. For example, for the potential unbound variable error above, the latest version of pyright does warn you. It also has built-in exhaustiveness checking &lt;strong&gt;without&lt;/strong&gt; needing the &lt;code class="docutils literal"&gt;assert_never&lt;/code&gt; technique. However, in one case it wasn’t working for me, until I finally tracked down the issue — mypy was able to handle this code and correctly deduce the base class of my enum, but pyright wasn’t:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_991936e2ec4e4e068e85670a718baf53-1" name="rest_code_991936e2ec4e4e068e85670a718baf53-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_991936e2ec4e4e068e85670a718baf53-1"&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_991936e2ec4e4e068e85670a718baf53-2" name="rest_code_991936e2ec4e4e068e85670a718baf53-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_991936e2ec4e4e068e85670a718baf53-2"&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_991936e2ec4e4e068e85670a718baf53-3" name="rest_code_991936e2ec4e4e068e85670a718baf53-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_991936e2ec4e4e068e85670a718baf53-3"&gt;&lt;/a&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_991936e2ec4e4e068e85670a718baf53-4" name="rest_code_991936e2ec4e4e068e85670a718baf53-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_991936e2ec4e4e068e85670a718baf53-4"&gt;&lt;/a&gt;    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;backports.strenum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StrEnum&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore [no-redef]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I eventually &lt;a class="reference external" href="https://github.com/microsoft/pyright/issues/4076"&gt;found an adequate solution&lt;/a&gt; that keeps them both happy — but only because I’m writing this blog post and don’t want to look stupid. Normally it would be “stuff is broken, maybe it’s me, maybe it’s them, gotta move on”.&lt;/p&gt;
&lt;p&gt;In addition, in some places pyright does &lt;strong&gt;not&lt;/strong&gt; support the &lt;code class="docutils literal"&gt;assert_never&lt;/code&gt; technique that mypy needs, and reports an error. There are &lt;a class="reference external" href="https://github.com/microsoft/pyright/issues/4706"&gt;other pain points&lt;/a&gt; if you try to use both.&lt;/p&gt;
&lt;p&gt;There are quite a few places where you find pyright doesn’t do the same thing as mypy because pyright is more correct. Microsoft people tend to know what they are talking about when it comes to type systems. But it means you may find yourself digging through &lt;a class="reference external" href="https://github.com/microsoft/pyright/issues?q=is%3Aissue+is%3Aclosed+label%3A%22as+designed%22"&gt;large numbers of issues closed with the “as designed” tag&lt;/a&gt; to find answers.&lt;/p&gt;
&lt;p&gt;[Update 2024: where I have the choice, I usually use exclusively pyright these days. I use it both as a standalone tool from CLI and in CI etc, and with the &lt;a class="reference external" href="https://github.com/emacs-lsp/lsp-pyright"&gt;lsp-pyright LSP server in Emacs&lt;/a&gt;. It now has enough support for &lt;code class="docutils literal"&gt;assert_never&lt;/code&gt; – it sometimes reports a warning for unreachable code, but that’s fine as it’s not an error. pyright seems to understand my Python code a lot better, especially when it comes to type narrowing. When I run mypy over the same bit of code, it reports a large number of spurious errors where pyright is correctly silent, and I don’t think there are many cases where mypy spots errors that pyright fails to see]&lt;/p&gt;
&lt;/section&gt;
&lt;section id="summary"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-15" role="doc-backlink"&gt;Summary&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Overall, despite listing more bad things than good, I’m actually happy with the addition of mypy as a required static type checker in this project.&lt;/p&gt;
&lt;p&gt;The disappointments I’ve listed may come from my experience and enjoyment of languages like Haskell where you really can lean on the type checker. In those languages, you find both that the rewards of static type checking are massively higher, and that the effort required to use them is massively lower. Haskell type signatures, for instance, are often not needed, and much easier to write and understand than Python’s.&lt;/p&gt;
&lt;p&gt;Perhaps the most positive outlook is “static type checking in Python is just an advanced linter, of course it’s not actually reliable”. This can be hard to accept, though, due to the amount of work you have to do to get any real benefit above and beyond linters like flake8 and ruff that, with virtually no changes to your code or workflow, catch a lot of issues with a very low false positive rate.&lt;/p&gt;
&lt;p&gt;In terms of tips and advice:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;You need to turn up error reporting and &lt;a class="reference external" href="https://rtpg.co/2023/03/07/how-to-adopt-mypy-on-bigger-projects.html"&gt;spend considerable effort configuring mypy&lt;/a&gt;, especially in larger projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you want something approaching reliability, your entire stack of libraries needs to have been designed with static types from the beginning, so you don’t have to use stubs. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;probably not much in stdlib. You’re going to need to wrap everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;probably not much that was created more than 5 years ago.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Maybe this works well for MegaCorps with an army of developers and a very large code base that they have to get under control somehow. I think for many projects, you are going to be happy with static type checking in Python only if you can resign yourself to a very low level of reliability, and are mostly leaning on other techniques for correctness, like an extensive test suite.&lt;/p&gt;
&lt;/section&gt;
&lt;hr class="docutils"&gt;
&lt;section id="links"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-16" 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/ecmsdo/python_type_hints_pyastgrep_case_study"&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="pyastgrep" label="pyastgrep"/>
    <category term="python" label="Python"/>
    <category term="python-type-hints" label="Python type hints"/>
  </entry>
  <entry>
    <title>The different uses of Python type hints</title>
    <id>https://lukeplant.me.uk/blog/posts/the-different-uses-of-python-type-hints/</id>
    <updated>2023-04-05T20:49:38+01:00</updated>
    <published>2023-04-05T20:49:38+01:00</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/the-different-uses-of-python-type-hints/"/>
    <summary type="html">&lt;p&gt;5 different things you might be using type annotations for, or might want to.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;When you use &lt;a class="reference external" href="https://peps.python.org/pep-0484/"&gt;type hints/annotations&lt;/a&gt; in &lt;a class="reference external" href="https://www.python.org/"&gt;Python&lt;/a&gt;, you could be using them for one or more of at least 5 different things:&lt;/p&gt;
&lt;section id="interactive-programming-help"&gt;
&lt;h2&gt;Interactive programming help&lt;/h2&gt;
&lt;p&gt;Many editors will be able to use type hints to give you help with:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;autocomplete (e.g. suggesting methods that actually exist on the type of objects you are dealing with)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;immediate error checking (e.g. squiggly red lines under mistakes)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;code navigation (e.g. jump to definition)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;refactoring (e.g. renaming a method and all uses of it)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To be clear, these features can often work without type hints – for example, I’ve used &lt;a class="reference external" href="https://github.com/davidhalter/jedi/"&gt;jedi&lt;/a&gt; very effectively to provide jump to definition etc. on code bases without any type hints, and many linters like &lt;a class="reference external" href="https://flake8.pycqa.org/en/latest/"&gt;flake8&lt;/a&gt; and &lt;a class="reference external" href="https://beta.ruff.rs/docs/"&gt;ruff&lt;/a&gt; also provide a lot of functionality without types. But type hints can help a lot in cases where static analysis wouldn’t otherwise give clear answers.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="static-type-checking"&gt;
&lt;h2&gt;Static type checking&lt;/h2&gt;
&lt;p&gt;This is where a tool uses the type annotations to check the correctness of your code. I’m distinguishing this from the “immediate error checking” use case above, even though the same tool such as &lt;a class="reference external" href="https://mypy.readthedocs.io/en/stable/"&gt;mypy&lt;/a&gt; or &lt;a class="reference external" href="https://microsoft.github.io/pyright/#/"&gt;pyright&lt;/a&gt; might be behind it, because I’m specifically thinking of cases where your code will be rejected by something in your process (like checks in your CI build system) if type checking reports errors. This case is different because help in your editor can be ignored if it is wrong, but static type checks built into your deployment processes etc. either cannot be skipped, or require extra work to ignore. “Friendly assistant” and “opinionated gatekeeper” are quite different personas, and you might not appreciate them equally.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="runtime-behaviour-determination"&gt;
&lt;h2&gt;Runtime behaviour determination&lt;/h2&gt;
&lt;p&gt;Running Python code can use reflection/introspection techniques to inspect type hints and change behaviour on that basis. The most obvious example in my mind is &lt;a class="reference external" href="https://docs.pydantic.dev/"&gt;pydantic&lt;/a&gt;, which uses type annotations to determine what correct inputs look like, and also serialisation/deserialisation behaviour. Another example would be runtime type checking like &lt;a class="reference external" href="https://beartype.readthedocs.io/en/latest/"&gt;beartype&lt;/a&gt; or &lt;a class="reference external" href="https://typeguard.readthedocs.io/en/latest/"&gt;typeguard&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; Another use is &lt;a class="reference external" href="https://lagom-di.readthedocs.io/en/latest/"&gt;dependency injection (Lagom)&lt;/a&gt; (thanks &lt;a class="reference external" href="https://lobste.rs/u/antoinewdg"&gt;antoinewdg&lt;/a&gt;), and other notable projects leaning on runtime use of type hints include &lt;a class="reference external" href="https://fastapi.tiangolo.com/"&gt;FastAPI&lt;/a&gt; and &lt;a class="reference external" href="https://typer.tiangolo.com/"&gt;Typer&lt;/a&gt; (thanks &lt;a class="reference external" href="https://www.b-list.org/"&gt;ubernostrum&lt;/a&gt;). There are probably lots more.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="code-documentation"&gt;
&lt;h2&gt;Code documentation&lt;/h2&gt;
&lt;p&gt;A type hint can be used to tell a user what kind of objects a function accepts or produces. This can be extracted in automatically created docs, or shown in your editor (in which case it also falls under “Interactive programming help”).&lt;/p&gt;
&lt;p&gt;An interesting application of this is &lt;a class="reference external" href="https://drf-spectacular.readthedocs.io/en/latest/"&gt;drf-spectacular&lt;/a&gt;, which uses type hints as well as other information to extract an OpenAPI spec from a project using &lt;a class="reference external" href="https://www.django-rest-framework.org/"&gt;DRF&lt;/a&gt;. This spec, as well as serving as documentation or input to tools like &lt;a class="reference external" href="https://github.com/Redocly/redoc"&gt;redoc&lt;/a&gt; or &lt;a class="reference external" href="https://swagger.io/tools/swagger-ui/"&gt;Swagger UI&lt;/a&gt;, can also be used for type checking or code generation in another language, typically for web frontend code, via tools like &lt;a class="reference external" href="https://github.com/OpenAPITools/openapi-generator"&gt;OpenAPI generator&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="compiler-instructions"&gt;
&lt;h2&gt;Compiler instructions&lt;/h2&gt;
&lt;p&gt;I don’t know how many people are doing this, but tools like &lt;a class="reference external" href="https://github.com/mypyc/mypyc"&gt;mypyc&lt;/a&gt; will use type hints to compile Python code to something faster, like C extensions. Using type annotations to speed up Python is &lt;a class="reference external" href="https://bernsteinbear.com//blog/typed-python/"&gt;quite hard in practice&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There isn’t really a point of this post other than to say “be aware of these different use cases”. This awareness can be very important when you are in any discussion about the usefulness or necessity of type hints – which scenarios are you thinking about?&lt;/p&gt;
&lt;p&gt;Also, when you are weighing up whether to add type hints, you might decide to do so in order to support some of these but not others – &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/"&gt;as I did for the parsy library&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, and this is a bit of a gotcha to leave you with, you may need to be very aware of the different use cases when thinking about correctness. If you see &lt;code class="docutils literal"&gt;count: int&lt;/code&gt;, what kind of guarantee do you have that the &lt;code class="docutils literal"&gt;count&lt;/code&gt; name is actually bound to an &lt;code class="docutils literal"&gt;int&lt;/code&gt; object at runtime? Is the type hint invoking some runtime checking, or is it merely docs, or hoping for a static type check that might not actually happen? You probably need to know which it is!&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/2beggz/different_uses_python_type_hints"&gt;Discussion of this post on Lobsters&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</content>
    <category term="python" label="Python"/>
    <category term="python-type-hints" label="Python type hints"/>
  </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>Python Type Hints: case study on parsy</title>
    <id>https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/</id>
    <updated>2022-11-21T21:07:02Z</updated>
    <published>2022-11-21T21:07:02Z</published>
    <author>
      <name>Luke Plant</name>
    </author>
    <link rel="alternate" type="text/html" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/"/>
    <summary type="html">&lt;p&gt;How I tried and failed to add static type checking to Parsy, and settled for type hints as documentation instead.&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;I have been trying to like static type checking in Python. For most of my Django projects, I get annoyed and give up, so I’ve had a go with some smaller projects instead. This blog post documents how it went with &lt;a class="reference external" href="https://github.com/python-parsy/parsy"&gt;Parsy&lt;/a&gt;, a parser combinator library I maintain.&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/python-type-hints-parsy-case-study/#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/python-type-hints-parsy-case-study/#intro-to-parsy" id="toc-entry-1"&gt;Intro to Parsy&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/python-type-hints-parsy-case-study/#simple-types" id="toc-entry-2"&gt;Simple types&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/python-type-hints-parsy-case-study/#generics" id="toc-entry-3"&gt;Generics&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/python-type-hints-parsy-case-study/#typed-parsy-fork" id="toc-entry-4"&gt;Typed Parsy fork&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/python-type-hints-parsy-case-study/#implementation-perspective" id="toc-entry-5"&gt;Implementation perspective&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/python-type-hints-parsy-case-study/#overall" id="toc-entry-6"&gt;Overall&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/python-type-hints-parsy-case-study/#using-it" id="toc-entry-7"&gt;Using it&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/python-type-hints-parsy-case-study/#sequences" id="toc-entry-8"&gt;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/python-type-hints-parsy-case-study/#error-messages" id="toc-entry-9"&gt;Error messages&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/python-type-hints-parsy-case-study/#generate-decorator" id="toc-entry-10"&gt;@generate decorator&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/python-type-hints-parsy-case-study/#overall-1" id="toc-entry-11"&gt;Overall&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/python-type-hints-parsy-case-study/#types-for-documentation" id="toc-entry-12"&gt;Types for documentation&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/python-type-hints-parsy-case-study/#conclusion" id="toc-entry-13"&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/python-type-hints-parsy-case-study/#links" id="toc-entry-14"&gt;Links&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/python-type-hints-parsy-case-study/#footnotes" id="toc-entry-15"&gt;Footnotes&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id="intro-to-parsy"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-1" role="doc-backlink"&gt;Intro to Parsy&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I need to explain a few things about Parsy.&lt;/p&gt;
&lt;p&gt;In Parsy, you build up &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; objects via a set of primitives and combinators. Each &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; object has a &lt;code class="docutils literal"&gt;parse&lt;/code&gt; method that accepts strings &lt;a class="brackets" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#strings" 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; and returns some object – which might be a string for your lowest building blocks, but quickly you build more complex parsers that return different types of objects. A lot of code is written in “fluent” style where you chain methods together.&lt;/p&gt;
&lt;p&gt;Here are some basics:&lt;/p&gt;
&lt;p&gt;The primitive &lt;code class="docutils literal"&gt;string&lt;/code&gt; just matches and returns the input:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_ce4f9922317d4b04862192f4c168f4bc-1" name="rest_code_ce4f9922317d4b04862192f4c168f4bc-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_ce4f9922317d4b04862192f4c168f4bc-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_ce4f9922317d4b04862192f4c168f4bc-2" name="rest_code_ce4f9922317d4b04862192f4c168f4bc-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_ce4f9922317d4b04862192f4c168f4bc-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_ce4f9922317d4b04862192f4c168f4bc-3" name="rest_code_ce4f9922317d4b04862192f4c168f4bc-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_ce4f9922317d4b04862192f4c168f4bc-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;hello&lt;/span&gt; &lt;span class="o"&gt;=&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;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_ce4f9922317d4b04862192f4c168f4bc-4" name="rest_code_ce4f9922317d4b04862192f4c168f4bc-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_ce4f9922317d4b04862192f4c168f4bc-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But we can change the result to some other type of object:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_51c1d034dd63495db0d437efeac58844-1" name="rest_code_51c1d034dd63495db0d437efeac58844-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_51c1d034dd63495db0d437efeac58844-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;true_parser&lt;/span&gt; &lt;span class="o"&gt;=&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;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&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_51c1d034dd63495db0d437efeac58844-2" name="rest_code_51c1d034dd63495db0d437efeac58844-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_51c1d034dd63495db0d437efeac58844-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;true_parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can map the parse result using a callable. This time I’m starting with a regex primitive, which returns strings, but converting to ints:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_fda97d53ddf34fe7bdf3160b70c17167-1" name="rest_code_fda97d53ddf34fe7bdf3160b70c17167-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_fda97d53ddf34fe7bdf3160b70c17167-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;int_parser&lt;/span&gt; &lt;span class="o"&gt;=&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;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_fda97d53ddf34fe7bdf3160b70c17167-2" name="rest_code_fda97d53ddf34fe7bdf3160b70c17167-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_fda97d53ddf34fe7bdf3160b70c17167-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;int_parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can discard things we don’t care about in a number of ways, such as with these “pointy” operators that point to the important bit:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_6053e2a6aa7b4c3b98e49613d0c8eeb2-1" name="rest_code_6053e2a6aa7b4c3b98e49613d0c8eeb2-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_6053e2a6aa7b4c3b98e49613d0c8eeb2-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;whitespace&lt;/span&gt; &lt;span class="o"&gt;=&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;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;"\s*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_6053e2a6aa7b4c3b98e49613d0c8eeb2-2" name="rest_code_6053e2a6aa7b4c3b98e49613d0c8eeb2-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_6053e2a6aa7b4c3b98e49613d0c8eeb2-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;whitespace&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;int_parser&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;whitespace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" 123    "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can have a sequence of items, here with some separator we don’t care about collecting:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_d125ff56d4a447c9a4945d1240203aab-1" name="rest_code_d125ff56d4a447c9a4945d1240203aab-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d125ff56d4a447c9a4945d1240203aab-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;three_ints&lt;/span&gt; &lt;span class="o"&gt;=&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;seq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_d125ff56d4a447c9a4945d1240203aab-2" name="rest_code_d125ff56d4a447c9a4945d1240203aab-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d125ff56d4a447c9a4945d1240203aab-2"&gt;&lt;/a&gt;  &lt;span class="n"&gt;int_parser&lt;/span&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;"-"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_d125ff56d4a447c9a4945d1240203aab-3" name="rest_code_d125ff56d4a447c9a4945d1240203aab-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d125ff56d4a447c9a4945d1240203aab-3"&gt;&lt;/a&gt;  &lt;span class="n"&gt;int_parser&lt;/span&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;"-"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_d125ff56d4a447c9a4945d1240203aab-4" name="rest_code_d125ff56d4a447c9a4945d1240203aab-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d125ff56d4a447c9a4945d1240203aab-4"&gt;&lt;/a&gt;  &lt;span class="n"&gt;int_parser&lt;/span&gt;
&lt;a id="rest_code_d125ff56d4a447c9a4945d1240203aab-5" name="rest_code_d125ff56d4a447c9a4945d1240203aab-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d125ff56d4a447c9a4945d1240203aab-5"&gt;&lt;/a&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_d125ff56d4a447c9a4945d1240203aab-6" name="rest_code_d125ff56d4a447c9a4945d1240203aab-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d125ff56d4a447c9a4945d1240203aab-6"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;three_ints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"123-45-67"&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="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we want something better than a list to store different components in (usually we do), we can use a keyword argument form of &lt;code class="docutils literal"&gt;seq&lt;/code&gt; to give names for the components and collect them in a dict instead of a list, and instead of &lt;code class="docutils literal"&gt;.map&lt;/code&gt; we can do &lt;code class="docutils literal"&gt;.combine_dict&lt;/code&gt; to convert the result into some other object:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-1" name="rest_code_9f7c901628164db68db73a76642ffe87-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-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_9f7c901628164db68db73a76642ffe87-2" name="rest_code_9f7c901628164db68db73a76642ffe87-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-3" name="rest_code_9f7c901628164db68db73a76642ffe87-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-3"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-4" name="rest_code_9f7c901628164db68db73a76642ffe87-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-5" name="rest_code_9f7c901628164db68db73a76642ffe87-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;year&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_9f7c901628164db68db73a76642ffe87-6" name="rest_code_9f7c901628164db68db73a76642ffe87-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;month&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_9f7c901628164db68db73a76642ffe87-7" name="rest_code_9f7c901628164db68db73a76642ffe87-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-7"&gt;&lt;/a&gt;    &lt;span class="n"&gt;day&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_9f7c901628164db68db73a76642ffe87-8" name="rest_code_9f7c901628164db68db73a76642ffe87-8" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-9" name="rest_code_9f7c901628164db68db73a76642ffe87-9" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-9"&gt;&lt;/a&gt;&lt;span class="n"&gt;date_parser&lt;/span&gt; &lt;span class="o"&gt;=&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;seq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-10" name="rest_code_9f7c901628164db68db73a76642ffe87-10" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-10"&gt;&lt;/a&gt;  &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="o"&gt;=&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;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="si"&gt;{4}&lt;/span&gt;&lt;span class="s2"&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;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;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;"-"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-11" name="rest_code_9f7c901628164db68db73a76642ffe87-11" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-11"&gt;&lt;/a&gt;  &lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="o"&gt;=&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;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="si"&gt;{2}&lt;/span&gt;&lt;span class="s2"&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;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;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;"-"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-12" name="rest_code_9f7c901628164db68db73a76642ffe87-12" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-12"&gt;&lt;/a&gt;  &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&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;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="si"&gt;{2}&lt;/span&gt;&lt;span class="s2"&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;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_9f7c901628164db68db73a76642ffe87-13" name="rest_code_9f7c901628164db68db73a76642ffe87-13" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-13"&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;combine_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-14" name="rest_code_9f7c901628164db68db73a76642ffe87-14" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-14"&gt;&lt;/a&gt;
&lt;a id="rest_code_9f7c901628164db68db73a76642ffe87-15" name="rest_code_9f7c901628164db68db73a76642ffe87-15" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9f7c901628164db68db73a76642ffe87-15"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;date_parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2022-11-19"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can have alternatives using the &lt;code class="docutils literal"&gt;|&lt;/code&gt; operator:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_f6b4b09b789a4189a2f5e7e2a10ad0e6-1" name="rest_code_f6b4b09b789a4189a2f5e7e2a10ad0e6-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_f6b4b09b789a4189a2f5e7e2a10ad0e6-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;bool_parser&lt;/span&gt; &lt;span class="o"&gt;=&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;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&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;span class="o"&gt;|&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;"false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_f6b4b09b789a4189a2f5e7e2a10ad0e6-2" name="rest_code_f6b4b09b789a4189a2f5e7e2a10ad0e6-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_f6b4b09b789a4189a2f5e7e2a10ad0e6-2"&gt;&lt;/a&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;bool_parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s enough to understand the rest of this post, let’s have a look at my 4 different approaches to improving the static type checking story for Parsy.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="simple-types"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-2" role="doc-backlink"&gt;Simple types&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The most obvious thing to do is to add &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; as the return value for a bunch of methods and operators, and other type hints wherever we can for the input arguments. For example, &lt;code class="docutils literal"&gt;.map&lt;/code&gt; is:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9e831f91d3b840828deb7de3d9b2580e-1" name="rest_code_9e831f91d3b840828deb7de3d9b2580e-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9e831f91d3b840828deb7de3d9b2580e-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map&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;map_function&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;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_9e831f91d3b840828deb7de3d9b2580e-2" name="rest_code_9e831f91d3b840828deb7de3d9b2580e-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9e831f91d3b840828deb7de3d9b2580e-2"&gt;&lt;/a&gt;   &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What type of object does the &lt;code class="docutils literal"&gt;parse&lt;/code&gt; method return? We don’t know, so we have to do:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_3446ba64125d48b7873d1dfaea31ca4b-1" name="rest_code_3446ba64125d48b7873d1dfaea31ca4b-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_3446ba64125d48b7873d1dfaea31ca4b-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&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="nb"&gt;input&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;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_3446ba64125d48b7873d1dfaea31ca4b-2" name="rest_code_3446ba64125d48b7873d1dfaea31ca4b-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_3446ba64125d48b7873d1dfaea31ca4b-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And this is our first hint that the static type checking isn’t very useful. For example, this faulty code will now type check:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_4f0e2d0793804c4eb56f4055f2b5cf65-1" name="rest_code_4f0e2d0793804c4eb56f4055f2b5cf65-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_4f0e2d0793804c4eb56f4055f2b5cf65-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;int_parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can see the return value is not going to be the right type, but our static type checker sees the &lt;code class="docutils literal"&gt;Any&lt;/code&gt; and allows it.&lt;/p&gt;
&lt;p&gt;The type checker is also not catching &lt;code class="docutils literal"&gt;TypeError&lt;/code&gt; exceptions for &lt;code class="docutils literal"&gt;.map()&lt;/code&gt; that it definitely ought to be able to. For example, suppose we have this faulty parser which is going to attempt to construct a &lt;code class="docutils literal"&gt;timedelta&lt;/code&gt; from strings like &lt;code class="docutils literal"&gt;"7 days ago"&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_a7d66053790a4578bcadc7416c1ffd3d-1" name="rest_code_a7d66053790a4578bcadc7416c1ffd3d-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_a7d66053790a4578bcadc7416c1ffd3d-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_a7d66053790a4578bcadc7416c1ffd3d-2" name="rest_code_a7d66053790a4578bcadc7416c1ffd3d-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_a7d66053790a4578bcadc7416c1ffd3d-2"&gt;&lt;/a&gt;&lt;span class="n"&gt;days_ago_parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&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;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;&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;" days ago"&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="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is going to fail every time with:&lt;/p&gt;
&lt;pre class="literal-block"&gt;TypeError: unsupported type for timedelta days component: str&lt;/pre&gt;
&lt;p&gt;That’s because we forgot a &lt;code class="docutils literal"&gt;.map(int)&lt;/code&gt; after the &lt;code class="docutils literal"&gt;P.regex&lt;/code&gt; parser.&lt;/p&gt;
&lt;p&gt;This kind of type error is caught for you by mypy and pyright if you try to pass a string to the &lt;code class="docutils literal"&gt;timedelta&lt;/code&gt; constructor, but here, we’ve got no constraints on the callable that would enable the type checker to pick it up. When we put a bare &lt;code class="docutils literal"&gt;Callable&lt;/code&gt; in a signature, as we did above for the &lt;code class="docutils literal"&gt;map&lt;/code&gt; method, we are really writing &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;Callable[...,&lt;/span&gt; Any]&lt;/code&gt;, so all proper type checking effectively gets disabled.&lt;/p&gt;
&lt;p&gt;If you want static type checking, this is not what you expect or want! It’s especially important for Parsy, because almost all the mistakes you are likely to make will be of this nature. Most Parsy code consists of parsers defined at a module level, which means that as soon as you import the module, you’ll know whether you have attempted to use combinator methods that don’t exist, for example, so there is little usefulness in a type checker being able to tell you this. What you want to know is whether you are going to get &lt;code class="docutils literal"&gt;TypeError&lt;/code&gt; or similar when you call the &lt;code class="docutils literal"&gt;parse&lt;/code&gt; method at runtime.&lt;/p&gt;
&lt;p&gt;Can we achieve that?&lt;/p&gt;
&lt;/section&gt;
&lt;section id="generics"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-3" role="doc-backlink"&gt;Generics&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The answer is to make the &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; type aware of what kind of object it is going to output. This can be achieved by parameterising the type of &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; with a generic type, and is the second main approach.&lt;/p&gt;
&lt;p&gt;Very often &lt;a class="reference external" href="https://mypy.readthedocs.io/en/stable/generics.html"&gt;generics&lt;/a&gt; are used for homogeneous containers, to capture the type of the object they contain. Here, we don’t have a container as such. We are instead capturing the type of the object that the parser instance is going to produce when you call &lt;code class="docutils literal"&gt;.parse()&lt;/code&gt; (assuming it succeeds, I’m ignoring all failure cases).&lt;/p&gt;
&lt;p&gt;Some of the key type signatures in our &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; code now look like this (lots of details elided):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-1" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-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;TypeVar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Generic&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-2" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-3" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-3"&gt;&lt;/a&gt;&lt;span class="n"&gt;OUT&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;"OUT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-4" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-4"&gt;&lt;/a&gt;&lt;span class="n"&gt;OUT1&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;"OUT1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-5" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-5"&gt;&lt;/a&gt;&lt;span class="n"&gt;OUT2&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;"OUT2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-6" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-7" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-8" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-8" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-8"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-9" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-9" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-9"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Result&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;OUT&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-10" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-10" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-10"&gt;&lt;/a&gt;    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OUT&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-11" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-11" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-11"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-12" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-12" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-12"&gt;&lt;/a&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-13" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-13" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-13"&gt;&lt;/a&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-14" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-14" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-14"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Parser&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;OUT&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-15" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-15" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-15"&gt;&lt;/a&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-16" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-16" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-16"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&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;stream&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;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-17" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-17" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-17"&gt;&lt;/a&gt;        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-18" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-18" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-18"&gt;&lt;/a&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-19" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-19" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-19"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map&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;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;map_function&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;OUT1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;OUT2&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;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;a id="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-20" name="rest_code_d9cb952a1e9a41e8826912dd2ee65a41-20" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_d9cb952a1e9a41e8826912dd2ee65a41-20"&gt;&lt;/a&gt;        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The main point is that we are capturing the type of output using a type parameter that our static type checker can track, which means that we can indeed catch the type errors that were ignored by the previous approach. I got this to work, and you can see my results in &lt;a class="reference external" href="https://github.com/python-parsy/parsy/pull/58"&gt;this PR&lt;/a&gt; (not merged).&lt;/p&gt;
&lt;p&gt;The reason it isn’t merged is that this approach breaks down as soon as you have things like &lt;code class="docutils literal"&gt;*args&lt;/code&gt; or &lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt; where the arguments need to be of different types. We have exactly that, multiple times, once you care about generics. For example, the &lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/ref/methods_and_combinators.html#parsy.seq"&gt;seq&lt;/a&gt; combinator takes sequence of parsers as input, and runs them in order, collecting their results. All of them are &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; instances, so that would work fine with the previous approach, but they could all have different output types. There is no way to specify a type signature for this, as well as for &lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/ref/methods_and_combinators.html#parsy.alt"&gt;alt&lt;/a&gt;, &lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/ref/methods_and_combinators.html#parsy.Parser.combine"&gt;combine&lt;/a&gt; and &lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/ref/methods_and_combinators.html#parsy.Parser.combine_dict"&gt;combine_dict&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The best you can do is specify that they return &lt;code class="docutils literal"&gt;Parser[Any]&lt;/code&gt;. This means you are downgrading to no type checking. This problem is going to apply to all but the most trivial cases – it’s difficult to come up with many real world examples where you don’t need sequencing.&lt;/p&gt;
&lt;p&gt;Some people would say “well, it works sometimes, so it is better than nothing”. The problem is that you when you start writing your parser, you may well really benefit from the type checking and start to lean on it. Then, as soon as you get beyond the level of your simple (single part) objects and are creating parsers for more complex (multiple part) objects in your language, the type checking silently disappears. Or, if you have strictness turned up high, your type checker will complain about the introduction of &lt;code class="docutils literal"&gt;Any&lt;/code&gt;, but you won’t be able to do anything about it.&lt;/p&gt;
&lt;p&gt;Both of these are really bad developer UX in my opinion. If the type checker is going to give up and go home at 2pm on the second day of work, it would be better for it not to show up, which would push developers to lean on other, more reliable methods, like writing tests.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="typed-parsy-fork"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-4" role="doc-backlink"&gt;Typed Parsy fork&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So we come to my third option, which builds on the second. Can we redesign Parsy so that it doesn’t have any &lt;code class="docutils literal"&gt;Any&lt;/code&gt;? This would be a backwards incompatible fork that removes any API that is impossible to fully type, and attempts to provide some good enough replacements.&lt;/p&gt;
&lt;p&gt;This was my most ambitious foray into static type checking in Python, and below are my notes from two perspectives – first implementation, which is important for any potential future contributors and maintainers, and secondly, usage, for the people who actually might want to use this fork.&lt;/p&gt;
&lt;section id="implementation-perspective"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-5" role="doc-backlink"&gt;Implementation perspective&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I didn’t complete this for reasons that will become clear. Overall, I’d say working “alongside” mypy and pyright was quite nice at points, and other times really difficult. To keep this article short, I’ve moved most of this section to footnotes. Here are the bullet points:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;p&gt;You can see my results in the &lt;a class="reference external" href="https://github.com/python-parsy/typed-parsy"&gt;typed-parsy&lt;/a&gt; repo, especially the &lt;a class="reference external" href="https://github.com/python-parsy/typed-parsy/blob/master/src/parsy/__init__.py"&gt;single source file&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I dropped support for anything but &lt;code class="docutils literal"&gt;str&lt;/code&gt; as input type, as a simplification.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I discovered that pyright can really shine in various places that mypy is lacking, particularly error messages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;But sometimes they fight each other &lt;a class="brackets" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#mypy-pyright-fight" id="footnote-reference-2" role="doc-noteref"&gt;&lt;span class="fn-bracket"&gt;[&lt;/span&gt;2&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I couldn’t work out how Protocols work with respect to operators and dunder methods. &lt;a class="brackets" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#protocols" id="footnote-reference-3" role="doc-noteref"&gt;&lt;span class="fn-bracket"&gt;[&lt;/span&gt;3&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Covariance is tricky, and you have to understand it. &lt;a class="brackets" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#covariance" id="footnote-reference-4" role="doc-noteref"&gt;&lt;span class="fn-bracket"&gt;[&lt;/span&gt;4&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/ref/primitives.html#parsy.forward_declaration"&gt;forward_declaration&lt;/a&gt; made my head explode. &lt;a class="brackets" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#forward-declaration-1" id="footnote-reference-5" role="doc-noteref"&gt;&lt;span class="fn-bracket"&gt;[&lt;/span&gt;5&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There are lots of places marked &lt;code class="docutils literal"&gt;TODO&lt;/code&gt; where I just couldn’t solve the new problems I had, even after getting rid of the most problematic code, and I had to give up and do &lt;code class="docutils literal"&gt;type: ignore&lt;/code&gt; quite a few times.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;section id="overall"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-6" role="doc-backlink"&gt;Overall&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Too much of this was just too hard, especially given we are only talking about a few hundred lines of code. It seemed much worse than doing the same thing in Haskell for some reason. This might be just because the language wasn’t designed for it. Even just the syntax for types is significantly worse.&lt;/p&gt;
&lt;p&gt;I think another issue is that there is no REPL for type level work. Normally when I’m trying to debug something, &lt;a class="reference external" href="https://lukeplant.me.uk/blog/posts/repl-python-programming-and-debugging-with-ipython/"&gt;I jump into a REPL&lt;/a&gt;. Working with actual, concrete values is so much easier, and so much closer to the &lt;a class="reference external" href="https://www.youtube.com/watch?v=PUv66718DII"&gt;immediate connection&lt;/a&gt; that makes programming enjoyable.&lt;/p&gt;
&lt;p&gt;An additional problem is that static type checkers have to worry about issues that may not be relevant to my code.&lt;/p&gt;
&lt;p&gt;Finally, the type system we have right now for Python is so far behind what Python can actually express. But it isn’t necessarily obvious when this is the case. The answer to “why doesn’t this work” is anywhere between, “I made a dumb mistake”, “I need to learn more”, “there’s a bug in mypy” and “that’s impossible (at the moment)”.&lt;/p&gt;
&lt;p&gt;As someone who needs to worry about future contributors and maintainers, these are serious issues. In addition to getting code to work, contributors would also have to get the type checks to pass, and ensure they weren’t breaking type checking for users, which is an extra burden.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="using-it"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-7" role="doc-backlink"&gt;Using it&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So much for the pains of implementing typed-parsy, what would it look like for a user?&lt;/p&gt;
&lt;p&gt;First, it happens that for a lot of typical usage, the user wouldn’t need to worry about types or adding type hints at all, but would still get type checking, which is great.&lt;/p&gt;
&lt;p&gt;Second, for the resulting code, mypy and pyright do a very good job of checking almost every type error you would normally make in your parsers. The few places where we lose type safety are limited and don’t result in &lt;code class="docutils literal"&gt;Any&lt;/code&gt; escaping and trashing everything from then on.&lt;/p&gt;
&lt;p&gt;However, if you do need to write type signatures, which you probably will if you have mypy settings turned up high and you want to make your own combinator functions (i.e. something that takes a Parser and returns a new Parser), which is fairly common, you’re going to need to understand a lot to create type hints that are both correct and useful.&lt;/p&gt;
&lt;p&gt;In addition, to achieve all this, we had to make some big sacrifices:&lt;/p&gt;
&lt;section id="sequences"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-8" role="doc-backlink"&gt;Sequences&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;You can’t implement &lt;code class="docutils literal"&gt;seq&lt;/code&gt;, &lt;code class="docutils literal"&gt;alt&lt;/code&gt;, &lt;code class="docutils literal"&gt;.combine&lt;/code&gt; or &lt;code class="docutils literal"&gt;.combine_dict&lt;/code&gt; in a type safe way (without degrading everything to &lt;code class="docutils literal"&gt;Parser[Any]&lt;/code&gt; from then on), and I had to remove them.&lt;/p&gt;
&lt;p&gt;The biggest issue is &lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/ref/methods_and_combinators.html?highlight=seq#parsy.seq"&gt;seq&lt;/a&gt;, and especially the convenience of the keyword argument version to name things. The alternative I came up with – using &lt;code class="docutils literal"&gt;&amp;amp;&lt;/code&gt; operator for creating a tuple of two results – does work, but turns out to be pretty ugly.&lt;/p&gt;
&lt;p&gt;Below are some incomplete extracts from the &lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/howto/other_examples.html#sql-select-statement-parser"&gt;SQL SELECT example&lt;/a&gt;, which illustrate the readability loss fairly well. We have some enum types and dataclasses to hold Abstract Syntax Tree nodes for a SQL parser:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-1" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Operator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-2" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;EQ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"="&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-3" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;LT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;"&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-4" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;GT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;"&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-5" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;LTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;="&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-6" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-6"&gt;&lt;/a&gt;    &lt;span class="n"&gt;GTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;="&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-7" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-7"&gt;&lt;/a&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-8" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-8" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-8"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-9" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-9" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-9"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comparison&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-10" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-10" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-10"&gt;&lt;/a&gt;    &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ColumnExpression&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-11" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-11" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-11"&gt;&lt;/a&gt;    &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Operator&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-12" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-12" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-12"&gt;&lt;/a&gt;    &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ColumnExpression&lt;/span&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-13" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-13" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-13"&gt;&lt;/a&gt;
&lt;a id="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-14" name="rest_code_e25d9e9fea0c437bbea42e2d0264c88a-14" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e25d9e9fea0c437bbea42e2d0264c88a-14"&gt;&lt;/a&gt;&lt;span class="c1"&gt;# dataclass for Select etc&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We then have a bunch of parsers for different components, which we assemble into larger parsers for bigger things, like &lt;code class="docutils literal"&gt;Comparison&lt;/code&gt; or &lt;code class="docutils literal"&gt;Select&lt;/code&gt;. With normal parsy it looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-1" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;=&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;seq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-2" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-2"&gt;&lt;/a&gt;    &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;column_expr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-3" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-3"&gt;&lt;/a&gt;    &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;=&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;from_enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Operator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-4" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-4"&gt;&lt;/a&gt;    &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;column_expr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-5" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-5"&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;combine_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Comparison&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-6" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-7" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-7"&gt;&lt;/a&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;=&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;"SELECT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-8" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-8" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-8"&gt;&lt;/a&gt;&lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;=&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;"FROM"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-9" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-9" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-9"&gt;&lt;/a&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;=&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;"WHERE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-10" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-10" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-10"&gt;&lt;/a&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-11" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-11" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-11"&gt;&lt;/a&gt;&lt;span class="n"&gt;select&lt;/span&gt; &lt;span class="o"&gt;=&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;seq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-12" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-12" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-12"&gt;&lt;/a&gt;    &lt;span class="n"&gt;_select&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-13" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-13" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-13"&gt;&lt;/a&gt;    &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;column_expr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sep_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;+&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;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&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_233e3b41dc1448e5bb0b7ebdc745605b-14" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-14" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-14"&gt;&lt;/a&gt;    &lt;span class="n"&gt;_from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-15" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-15" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-15"&gt;&lt;/a&gt;    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-16" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-16" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-16"&gt;&lt;/a&gt;    &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;space&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-17" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-17" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-17"&gt;&lt;/a&gt;    &lt;span class="n"&gt;_end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;+&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;";"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;a id="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-18" name="rest_code_233e3b41dc1448e5bb0b7ebdc745605b-18" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_233e3b41dc1448e5bb0b7ebdc745605b-18"&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;combine_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There are some things you need to understand: &lt;code class="docutils literal"&gt;seq&lt;/code&gt; runs a sequence of parsers in order, and with its keyword arguments version allows you to give names to each one, to produce a dictionary of results. &lt;code class="docutils literal"&gt;.combine_dict&lt;/code&gt; then passes these to a callable using &lt;code class="docutils literal"&gt;**kwargs&lt;/code&gt; syntax.&lt;/p&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;.combine_dict&lt;/code&gt; also has a neat trick of skipping items whose names start with
underscores, to allow you to deal with things that you need to parse but want to
discard, like &lt;code class="docutils literal"&gt;_select&lt;/code&gt;, &lt;code class="docutils literal"&gt;_from&lt;/code&gt; and &lt;code class="docutils literal"&gt;_end&lt;/code&gt; above. Notice how easy it is
to read the &lt;code class="docutils literal"&gt;select&lt;/code&gt; parser and see what things we are picking out.&lt;/p&gt;
&lt;p&gt;With typed-parsy, this was the best I could do, formatted using Black:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-1" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-2" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-2"&gt;&lt;/a&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column_expr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;from_enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Operator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;column_expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-3" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-3"&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;map&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;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Comparison&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&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;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&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;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-4" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-4"&gt;&lt;/a&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-5" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-5"&gt;&lt;/a&gt;&lt;span class="n"&gt;SELECT&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;"SELECT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-6" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-6"&gt;&lt;/a&gt;&lt;span class="n"&gt;FROM&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;"FROM"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-7" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-7"&gt;&lt;/a&gt;&lt;span class="n"&gt;WHERE&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;"WHERE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-8" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-8" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-8"&gt;&lt;/a&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-9" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-9" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-9"&gt;&lt;/a&gt;&lt;span class="n"&gt;select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-10" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-10" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-10"&gt;&lt;/a&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-11" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-11" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-11"&gt;&lt;/a&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-12" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-12" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-12"&gt;&lt;/a&gt;        &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column_expr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sep_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;padding&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;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&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_9ef0a1b1794642adab7a71802aa6b5f1-13" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-13" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-13"&gt;&lt;/a&gt;        &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-14" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-14" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-14"&gt;&lt;/a&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-15" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-15" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-15"&gt;&lt;/a&gt;    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-16" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-16" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-16"&gt;&lt;/a&gt;    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;space&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;padding&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;";"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;a id="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-17" name="rest_code_9ef0a1b1794642adab7a71802aa6b5f1-17" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_9ef0a1b1794642adab7a71802aa6b5f1-17"&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;map&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;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&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;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&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;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is a pretty massive regression. We can’t use commas to separate parts as before, and we can’t use keyword arguments to name components any more. Instead we have to use tuples, and when we end up with nested tuples it’s awful – I literally couldn’t work out how to write the tuple indexing correctly and had to just keep guessing until I got it right. And that’s just with 3 items, some parsers might have many more items in a sequence, each of which adds another level of nesting.&lt;/p&gt;
&lt;p&gt;This might have been significantly better if we still had tuple unpacking within function/lambdas signatures (which was removed in Python 3), but still not very nice.&lt;/p&gt;
&lt;p&gt;It is kind of impressive that mypy and pyright will handle all this and tell you about type violations very reliably. This is possible because they have support for indexing tuples i.e. in the above statements it can tell you what the types of &lt;code class="docutils literal"&gt;t[0]&lt;/code&gt;, &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;t[0][1]&lt;/span&gt;&lt;/code&gt; etc are. In an IDE, tools like pyright will tell you what you need to supply for &lt;code class="docutils literal"&gt;.map()&lt;/code&gt; – for example for the &lt;code class="docutils literal"&gt;select&lt;/code&gt; statement, inside the final &lt;code class="docutils literal"&gt;.map&lt;/code&gt; call:&lt;/p&gt;
&lt;pre class="literal-block"&gt;(map_fn: (tuple[tuple[list[Field | String | Number], Table], Comparison | None]) -&amp;gt; OUT2@map) -&amp;gt; Parser[OUT2@map]",&lt;/pre&gt;
&lt;p&gt;But this isn’t my idea of developer friendly. The loss of readability is huge, even for simple cases.&lt;/p&gt;
&lt;p&gt;For comparison, I looked at &lt;a class="reference external" href="https://funcparserlib.pirx.ru/"&gt;funcparserlib&lt;/a&gt;, the Python parsing library closest to Parsy. They claim “fully typed”, but it turns out that their sequencing operator, which returns only tuples and so isn’t as usable as &lt;code class="docutils literal"&gt;seq&lt;/code&gt;, flattens nested tuples. This is much better for usability, but is also impossible to type, so they introduce &lt;code class="docutils literal"&gt;Any&lt;/code&gt; &lt;a class="reference external" href="https://github.com/vlasovskikh/funcparserlib/blob/5af4f8cc445d3f919590b8729430c576d4426917/funcparserlib/parser.pyi#L66"&gt;at this point&lt;/a&gt;, and so lose type checking, the thing I’ve been trying to avoid.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2022-11-24:&lt;/strong&gt; I had another idea about how to approach this. Parsy could provide &lt;code class="docutils literal"&gt;seq2&lt;/code&gt;, &lt;code class="docutils literal"&gt;seq3&lt;/code&gt;, &lt;code class="docutils literal"&gt;seq4&lt;/code&gt; etc. combinators which return 2-tuples, 3-tuples, 4-tuples etc. These functions, which would have to be implemented the long way, would handle the nested tuple unpacking for you, without losing type safety. As an overload, they could also optionally include the functionality of &lt;code class="docutils literal"&gt;.combine&lt;/code&gt; without loss of safety, and in this way you would get close to the usability of the &lt;code class="docutils literal"&gt;&lt;span class="pre"&gt;seq(*args)&lt;/span&gt;&lt;/code&gt; version, with just the annoyance that you have to change the function if you want to add another argument. This still wouldn’t give you the usability of the keyword argument version of &lt;code class="docutils literal"&gt;seq&lt;/code&gt;, but it would probably be a significant improvement.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="error-messages"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-9" role="doc-backlink"&gt;Error messages&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;If you are relying on types to fix you up, instead of readable code, then you depend on the error messages your type checker will emit. Below is an example of an error I got when attempting to port the example code in &lt;code class="docutils literal"&gt;simple_logo_lexer.py&lt;/code&gt;, which has a function &lt;code class="docutils literal"&gt;flatten_list&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="literal-block"&gt;Argument 1 to "map" of "Parser" has incompatible type
"Callable[[List[List[T]]], List[T]]"; expected
"Callable[[List[Tuple[Tuple[str, int], str]]], List[T]]"&lt;/pre&gt;
&lt;p&gt;Here was another one I hit:&lt;/p&gt;
&lt;pre class="literal-block"&gt;Argument 1 to "map" of "Parser" has incompatible type
"Callable[[Tuple[List[List[Tuple[List[List[OUT]], List[OUT]]]],
List[Tuple[List[List[OUT]], List[OUT]]]]], List[List[Tuple[List[List[OUT]],
List[OUT]]]]]";
expected "Callable[[Tuple[List[List[Tuple[List[List[OUT]],
List[OUT]]]], List[Tuple[List[List[OUT]], List[OUT]]]]], List[OUT]]"&lt;/pre&gt;
&lt;p&gt;I can’t remember what mistake produced that, but I did notice that the code worked perfectly at runtime. Also, pyright didn’t complain, only mypy. This is the kind of thing that makes people hate static typing.&lt;/p&gt;
&lt;p&gt;One of the main principles of parsy is that it should be very easy to pull data into appropriate containers where every field is &lt;strong&gt;named&lt;/strong&gt;, as well as &lt;strong&gt;typed&lt;/strong&gt; – rather than a parse that returns lists of nested lists and tuples and dicts. This is why &lt;code class="docutils literal"&gt;namedtuple&lt;/code&gt; is a big improvement over &lt;code class="docutils literal"&gt;tuple&lt;/code&gt;, and &lt;a class="reference external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt; are a big step up again. But at the level of types and error messages, it seems we are back in the dark ages, with nested tuples and lists everywhere.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="generate-decorator"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-10" role="doc-backlink"&gt;@generate decorator&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Being able to add conditional logic and control flow into parsers is really important for some cases, and Parsy has an elegant solution in the form of the &lt;a class="reference external" href="https://parsy.readthedocs.io/en/latest/tutorial.html#using-previously-parsed-values"&gt;@generate decorator&lt;/a&gt;. Getting this to work in typed-parsy turned out to be only partially possible.&lt;/p&gt;
&lt;p&gt;The first issue is that, unlike other ways of building up parsers, the user will need to write a type signature to get type checking, and it’s complex enough that there is no reasonable way for someone to understand what type signature they need without looking up the docs, which is poor usability.&lt;/p&gt;
&lt;p&gt;Having done so, they can get type checking on the return type of the parser, and code that uses that parser. However, they get no type checking related to parsers used within the function. The &lt;code class="docutils literal"&gt;Generator&lt;/code&gt; type assumes a homogeneous stream of yield and send types, whereas we have pairs of yield/send types which need to match within the pair, but each pair can be completely different from the next in the stream.&lt;/p&gt;
&lt;p&gt;Since you can’t sacrifice &lt;code class="docutils literal"&gt;@generate&lt;/code&gt; without major loss of functionality/usability, you have to live with the fact that you do not have type safety in the body of a &lt;code class="docutils literal"&gt;@generate&lt;/code&gt; function.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="overall-1"&gt;
&lt;h4&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-11" role="doc-backlink"&gt;Overall&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;This did not feel like an upgrade for a user, but rather like a pretty big downgrade. typed-parsy was definitely going to be worse than parsy, so I stopped working on it.&lt;/p&gt;
&lt;p&gt;Which brings me to my last approach:&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="types-for-documentation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-12" role="doc-backlink"&gt;Types for documentation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At some point along the way, I noticed that for the original version of parsy, with no type hints at all, my language server (pyright) was able to correctly infer return types of all the methods that returned &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; instances. The types it was inferring were the same as I would have added in the very first section, simple &lt;code class="docutils literal"&gt;Parser&lt;/code&gt; objects, and that meant it could reliably give help with chained method calls, which is pretty nice.&lt;/p&gt;
&lt;p&gt;The biggest problems were that the docstrings weren’t helpful (in most cases missing), and that for many parameters it wasn’t entirely obvious what type of object you should be passing in.&lt;/p&gt;
&lt;p&gt;So, using a small amount of effort, we could improve usability a lot. We can add those dosctrings, and add type hints that are about the same level of types that pyright was inferring anyway, just a bit more complete.&lt;/p&gt;
&lt;p&gt;The one thing I don’t want to do is imply that these types bring Parsy code up to the level of being “type checked”, but there is a simple way I can do that – by not including a &lt;code class="docutils literal"&gt;py.typed&lt;/code&gt; &lt;a class="reference external" href="https://peps.python.org/pep-0561/"&gt;marker file&lt;/a&gt; to my package.&lt;/p&gt;
&lt;p&gt;So, I’m back at the beginning, but with a different aim. Now, it’s not about helping automated static type checkers – without a &lt;code class="docutils literal"&gt;py.typed&lt;/code&gt; marker in the module they basically ignore the types – it’s about improving usability for developers as they write. I’ve done this work in the master branch now, and will hopefully release it soon.&lt;/p&gt;
&lt;p&gt;There is another interesting advantage to this: because I’ve given up on static type checks and I’m not using static type checking for internal use in Parsy at all, I can be a bit looser with the type hints, allowing for greater readability. For example, I can annotate an optional string argument as &lt;code class="docutils literal"&gt;arg: str = None&lt;/code&gt;, even though that’s not strictly compliant with PEP 484. In other places I can use slightly simplified type hints for the sake of having something that doesn’t make your eyes glaze over, even if it doesn’t show all the possibilities – such as saying that input types can be &lt;code class="docutils literal"&gt;str | bytes | list&lt;/code&gt;, when technically I should be writing something much more abstract and complicated using &lt;code class="docutils literal"&gt;Sequence&lt;/code&gt;.&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/python-type-hints-parsy-case-study/#toc-entry-13" role="doc-backlink"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the end, type hints didn’t work out for use by a static type checker. But as clear and concise documentation for humans that pop up in a code editor, they worked well. Unless or until there are some big improvements in what it is possible to express with static types in Python, this seems to be the best solution for Parsy.&lt;/p&gt;
&lt;p&gt;I learnt a lot about the limitations of typing in Python along the way, and hope you found this helpful too!&lt;/p&gt;
&lt;/section&gt;
&lt;hr class="docutils"&gt;
&lt;section id="links"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#toc-entry-14" 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/1elwat/python_type_hints_case_study_on_parsy"&gt;Discussion of this posts on Lobsters&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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/python-type-hints-parsy-case-study/#toc-entry-15" role="doc-backlink"&gt;Footnotes&lt;/a&gt;&lt;/h2&gt;
&lt;aside class="footnote-list brackets"&gt;
&lt;aside class="footnote brackets" id="strings" 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/python-type-hints-parsy-case-study/#footnote-reference-1"&gt;1&lt;/a&gt;&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;Parser input:&lt;/p&gt;
&lt;p&gt;Actually Parsy supports bytes and in fact any sequence of objects as input. But I’m ignoring that for the rest of the post for the sake of simplicity.&lt;/p&gt;
&lt;/aside&gt;
&lt;aside class="footnote brackets" id="mypy-pyright-fight" 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/python-type-hints-parsy-case-study/#footnote-reference-2"&gt;2&lt;/a&gt;&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;mypy and pyright can fight: For example, when implementing the operators that discard one of the parsers, pyright complained (I think rightly) about unused generic parameters. When I removed them, mypy wanted them put back in!&lt;/p&gt;
&lt;/aside&gt;
&lt;aside class="footnote brackets" id="protocols" 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/python-type-hints-parsy-case-study/#footnote-reference-3"&gt;3&lt;/a&gt;&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;Protocols and operators:&lt;/p&gt;
&lt;p&gt;I wanted some way of expressing “an object that supports addition”, or actually “an object that supports addition and returns an object of the same type”. I needed this for the &lt;code class="docutils literal"&gt;+&lt;/code&gt; operator, which you can use for both things like &lt;code class="docutils literal"&gt;Parser[str]&lt;/code&gt; and &lt;code class="docutils literal"&gt;Parser[list[str]]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But I couldn’t get this minimal test case to work:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_e5c41fe9a8124c9d9184cafcef113f37-1" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Addable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&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_e5c41fe9a8124c9d9184cafcef113f37-2" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-2"&gt;&lt;/a&gt;    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
&lt;a id="rest_code_e5c41fe9a8124c9d9184cafcef113f37-3" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-3"&gt;&lt;/a&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__add__&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;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&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;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_e5c41fe9a8124c9d9184cafcef113f37-4" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-4"&gt;&lt;/a&gt;        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;a id="rest_code_e5c41fe9a8124c9d9184cafcef113f37-5" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-5"&gt;&lt;/a&gt;
&lt;a id="rest_code_e5c41fe9a8124c9d9184cafcef113f37-6" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-6"&gt;&lt;/a&gt;
&lt;a id="rest_code_e5c41fe9a8124c9d9184cafcef113f37-7" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-7"&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;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Addable&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;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Addable&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;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;a id="rest_code_e5c41fe9a8124c9d9184cafcef113f37-8" name="rest_code_e5c41fe9a8124c9d9184cafcef113f37-8" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_e5c41fe9a8124c9d9184cafcef113f37-8"&gt;&lt;/a&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Mypy reports:&lt;/p&gt;
&lt;pre class="literal-block"&gt;Unsupported operand types for + ("Addable[T]" and "Addable[T]")  [operator]&lt;/pre&gt;
&lt;p&gt;This is probably my fault, but I couldn’t work it out, I obviously need to understand more about protocols.&lt;/p&gt;
&lt;/aside&gt;
&lt;aside class="footnote brackets" id="covariance" 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/python-type-hints-parsy-case-study/#footnote-reference-4"&gt;4&lt;/a&gt;&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;Covariance:&lt;/p&gt;
&lt;p&gt;As a replacement for the variadic &lt;code class="docutils literal"&gt;seq&lt;/code&gt; which produces a hetereogeneous list (in its simplest form), I added the &lt;code class="docutils literal"&gt;&amp;amp;&lt;/code&gt; operator which returns a tuple. This can be typed correctly, because unlike &lt;code class="docutils literal"&gt;list&lt;/code&gt;/&lt;code class="docutils literal"&gt;typing.List&lt;/code&gt; which is considered to be a homogeneous container, &lt;code class="docutils literal"&gt;tuple&lt;/code&gt;/&lt;code class="docutils literal"&gt;typing.Tuple&lt;/code&gt; is treated as a product type, like it is in other languages.&lt;/p&gt;
&lt;p&gt;This pairs well with the existing &lt;code class="docutils literal"&gt;|&lt;/code&gt; operator for alternatives, which produces a union (or sum type).&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_585b4745c93d4cfeae25effa463db7d0-1" name="rest_code_585b4745c93d4cfeae25effa463db7d0-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_585b4745c93d4cfeae25effa463db7d0-1"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__or__&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;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT2&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;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OUT2&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
&lt;a id="rest_code_585b4745c93d4cfeae25effa463db7d0-2" name="rest_code_585b4745c93d4cfeae25effa463db7d0-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_585b4745c93d4cfeae25effa463db7d0-2"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;a id="rest_code_585b4745c93d4cfeae25effa463db7d0-3" name="rest_code_585b4745c93d4cfeae25effa463db7d0-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_585b4745c93d4cfeae25effa463db7d0-3"&gt;&lt;/a&gt;
&lt;a id="rest_code_585b4745c93d4cfeae25effa463db7d0-4" name="rest_code_585b4745c93d4cfeae25effa463db7d0-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_585b4745c93d4cfeae25effa463db7d0-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__and__&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;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT2&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;Parser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OUT1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OUT2&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
&lt;a id="rest_code_585b4745c93d4cfeae25effa463db7d0-5" name="rest_code_585b4745c93d4cfeae25effa463db7d0-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_585b4745c93d4cfeae25effa463db7d0-5"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In implementing the first, however, both mypy and pyright complain:&lt;/p&gt;
&lt;pre class="literal-block"&gt;Expression of type "Result[OUT1@__or__]" cannot be assigned to return type
"Result[OUT1@__or__ | OUT2@__or__]"&lt;/pre&gt;
&lt;p&gt;The issue here is that the type checker needs to know that a &lt;code class="docutils literal"&gt;Result[A]&lt;/code&gt; is a sub-type of a &lt;code class="docutils literal"&gt;Result[A | B]&lt;/code&gt;, which is only true if &lt;code class="docutils literal"&gt;Result&lt;/code&gt; is &lt;strong&gt;covariant&lt;/strong&gt; with respect to that type parameter. Since it’s an immutable container of a single item, whose only operation is that you can extract the item, it is covariant.&lt;/p&gt;
&lt;p&gt;My first attempt to fix this, however, was to change all the type parameters to &lt;code class="docutils literal"&gt;covariant=True&lt;/code&gt;, which gave me a ton more problems – both mypy and pyright complaining "covariant type variable cannot be used in parameter type" in many places.&lt;/p&gt;
&lt;p&gt;It turns out, after a lot of attempts, head scratching, and finally the fresh take involved in writing a blog post, I just needed to use a covariant type parameter only for the definition of &lt;code class="docutils literal"&gt;Result&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code python"&gt;&lt;a id="rest_code_1ae54fdc40df46a7acc1403baab4ba10-1" name="rest_code_1ae54fdc40df46a7acc1403baab4ba10-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_1ae54fdc40df46a7acc1403baab4ba10-1"&gt;&lt;/a&gt;&lt;span class="n"&gt;OUT_co&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;"OUT_co"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;covariant&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_1ae54fdc40df46a7acc1403baab4ba10-2" name="rest_code_1ae54fdc40df46a7acc1403baab4ba10-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_1ae54fdc40df46a7acc1403baab4ba10-2"&gt;&lt;/a&gt;
&lt;a id="rest_code_1ae54fdc40df46a7acc1403baab4ba10-3" name="rest_code_1ae54fdc40df46a7acc1403baab4ba10-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_1ae54fdc40df46a7acc1403baab4ba10-3"&gt;&lt;/a&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;a id="rest_code_1ae54fdc40df46a7acc1403baab4ba10-4" name="rest_code_1ae54fdc40df46a7acc1403baab4ba10-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_1ae54fdc40df46a7acc1403baab4ba10-4"&gt;&lt;/a&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Result&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;OUT_co&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
&lt;a id="rest_code_1ae54fdc40df46a7acc1403baab4ba10-5" name="rest_code_1ae54fdc40df46a7acc1403baab4ba10-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_1ae54fdc40df46a7acc1403baab4ba10-5"&gt;&lt;/a&gt;    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OUT_co&lt;/span&gt;
&lt;a id="rest_code_1ae54fdc40df46a7acc1403baab4ba10-6" name="rest_code_1ae54fdc40df46a7acc1403baab4ba10-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study/#rest_code_1ae54fdc40df46a7acc1403baab4ba10-6"&gt;&lt;/a&gt;    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I think this is now correct, and the fact that I’m not using the &lt;code class="docutils literal"&gt;OUT_co&lt;/code&gt; type parameter in all the other places is not a problem, but to be honest I’m not entirely sure. Both mypy and pyright now seem happy, so I think I’m done?&lt;/p&gt;
&lt;/aside&gt;
&lt;aside class="footnote brackets" id="forward-declaration-1" 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/python-type-hints-parsy-case-study/#footnote-reference-5"&gt;5&lt;/a&gt;&lt;span class="fn-bracket"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;p&gt;&lt;code class="docutils literal"&gt;forward_declaration&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;This is a neat way of untying recursive definitions. In typical uses, I think the type hint for the object produced also requires a recursive definition. Every time I looked at to work out how you would declare a parser using &lt;code class="docutils literal"&gt;forward_declaration&lt;/code&gt; such that a type checker could still work, my brain just shut down. I think it would probably require users having to use forward references at the type level, but might also hit some unsolvable problems in how that interacts with &lt;code class="docutils literal"&gt;forward_declaration&lt;/code&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;/aside&gt;
&lt;/section&gt;</content>
    <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>
</feed>
