Luke Plant's home pagehttps://lukeplant.me.uk/blog/atom/index.xml2024-01-22T08:57:30ZLuke PlantNikolaPython packaging must be getting better - a datapointhttps://lukeplant.me.uk/blog/posts/python-packaging-must-be-getting-better-a-datapoint/2024-01-21T20:47:10Z2024-01-21T20:47:10ZLuke Plant<p>I “pip install”ed my app on Windows and everything just worked. Something is going right.</p><p>I’m developing some Python software for a client, which in its current early state is desktop software that will need to run on Windows.</p>
<p>So far, however, I have done all development on my normal comfortable Linux machine. I haven’t really used Windows in earnest for more than 15 years – to the point where my wife happily installs Linux on her own machine, knowing that I’ll be hopeless at helping her fix issues if the OS is Windows – and certainly not for development work in that time. So I was expecting a fair amount of pain.</p>
<p>There was certainly a lot of friction getting a development environment set up. <a class="reference external" href="https://realpython.com/python-coding-setup-windows/">RealPython.com have a great guide</a> which got me a long way, but even that had some holes and a lot of inconvenience, mostly due to the fact that, on the machine I needed to use, my main login and my admin login are separate. (I’m very lucky to be granted an admin login at all, so I’m not complaining). And there are lots of ways that Windows just seems to be broken, but that’s another blog post.</p>
<p>When it came to getting my app running, however, I was very pleasantly surprised.</p>
<p>At this stage in development, I just have a rough <code class="docutils literal">requirements.txt</code> that I add Python deps to manually. This might be a good thing, as I avoid the pain/confusion of <a class="reference external" href="https://chriswarrick.com/blog/2024/01/15/python-packaging-one-year-later/">some of the additional layers people have added</a>, which I’m mostly avoiding until things settle down a bit.</p>
<p>So after installing Python and creating a virtual environment on Windows, I ran <code class="docutils literal">pip install <span class="pre">-r</span> requirements.txt</code>, expecting a world of pain, especially as I already had complex non-Python dependencies, including <a class="reference external" href="https://doc.qt.io/qt-5/index.html">Qt5</a> and <a class="reference external" href="https://vtk.org/">VTK</a>. I had specified both of these as simple Python deps via the wrappers <a class="reference external" href="https://pypi.org/project/PyQt5/">pyqt5</a> and <a class="reference external" href="https://pypi.org/project/vtk/">vtk</a> in my <code class="docutils literal">requirements.txt</code>, and nothing else, with the attitude of “well I may as well dream this is going to work”.</p>
<p>And in fact, it did! Everything just downloaded as binary wheels – rather large ones, but that’s fine. I didn’t need compilers or QMake or header files or anything.</p>
<p>And when I ran my app, apart from a dependency that I’d forgotten to add to <code class="docutils literal">requirements.txt</code>, <strong>everything worked perfectly first time</strong>. This was even more surprising as I had put zero conscious effort into Windows compatibility. In retrospect I realise that use of <a class="reference external" href="https://docs.python.org/3/library/pathlib.html">pathlib</a>, which is automatic for me these days, had helped me because it smooths over some Windows/Unix differences with path handling.</p>
<p>Of course, this is a single datapoint. From other people’s reports there are many, many ways that this experience may not be typical. But that it is possible at all suggests that a lot of progress has been made and we are very much going in the right direction. A lot of people have put a lot of work in to achieve that, for which I’m very grateful!</p>Python Type Hints: pyastgrep case studyhttps://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/2023-10-05T09:45:02Z2023-10-05T09:45:02ZLuke Plant<p>A second, and more successful attempt to use static type checking in a real Python project</p><p>In a previous post, I did <a class="reference external" href="https://lukeplant.me.uk/blog/posts/python-type-hints-parsy-case-study">a case study on my attempts to add type hints to parsy</a>. In this post, I’m continuing the series, but in a very different project.</p>
<p>A while back I forked an existing tool called <a class="reference external" href="https://github.com/hchasestevens/astpath">astpath</a> to create my own tool <a class="reference external" href="https://github.com/spookylukey/pyastgrep/">pyastgrep</a>, 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.</p>
<p>There are at least <a class="reference external" href="https://lukeplant.me.uk/blog/posts/the-different-uses-of-python-type-hints">5 different ways that type hints can be used in Python</a>, 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.</p>
<nav class="contents" id="contents" role="doc-toc">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#about-pyastgrep" id="toc-entry-1">About pyastgrep</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#things-i-liked" id="toc-entry-2">Things I liked</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#issues" id="toc-entry-3">Issues</a></p>
<ul>
<li><p><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">mypy just isn’t checking that code.</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#iobase-vs-binaryio" id="toc-entry-5">IOBase vs BinaryIO</a></p></li>
<li><p><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">Missing types for imports</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#equality-checks" id="toc-entry-7">Equality checks</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#mypy-caching-bugs" id="toc-entry-8">mypy caching bugs</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#third-party-types" id="toc-entry-9">Third party types</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#duck-typing" id="toc-entry-10">Duck typing</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#false-security" id="toc-entry-11">False security</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#exhaustiveness-checking" id="toc-entry-12">Exhaustiveness checking</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#decorators" id="toc-entry-13">Decorators</a></p></li>
</ul>
</li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#pyright" id="toc-entry-14">pyright</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#summary" id="toc-entry-15">Summary</a></p></li>
<li><p><a class="reference internal" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#links" id="toc-entry-16">Links</a></p></li>
</ul>
</nav>
<section id="about-pyastgrep">
<h2><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-1" role="doc-backlink">About pyastgrep</a></h2>
<p>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:</p>
<ul class="simple">
<li><p>intelligently work out which files to search (respecting .gitignore files etc.)</p></li>
<li><p>parse the Python files as AST and convert to XML</p></li>
<li><p>apply a user-supplied XPath expression to search for specific AST elements</p></li>
<li><p>print the results (with the complexity of handling different context strategies and colouring)</p></li>
</ul>
<p>Here is an example showing pyastgrep searching its own code base for all usages of names (variables etc) that contain the substring <code class="docutils literal">idx</code>:</p>
<pre style="background-color: #000; color: #fff;">
<span>$ pyastgrep './/Name[contains(@id, "idx")]'
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span style=" ">:</span><span style="color: #67b11d; font-weight: bold; ">60</span><span>:</span><span style="color: #da8b55; ">5</span><span>: </span><span> </span><span style="color: #f2241f; ">current_idx</span><span> = 0
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">64</span><span>:</span><span style="color: #da8b55; ">9</span><span>: </span><span> </span><span style="color: #f2241f; ">linebreak_idx</span><span> = python_file_bytes.find(b"\n", current_idx)
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">64</span><span>:</span><span style="color: #da8b55; ">55</span><span>: </span><span> linebreak_idx = python_file_bytes.find(b"\n", </span><span style="color: #f2241f; ">current_idx</span><span>)
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">65</span><span>:</span><span style="color: #da8b55; ">12</span><span>: </span><span> if </span><span style="color: #f2241f; ">linebreak_idx</span><span> < 0:
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">66</span><span>:</span><span style="color: #da8b55; ">38</span><span>: </span><span> line = python_file_bytes[</span><span style="color: #f2241f; ">current_idx</span><span>:]
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">68</span><span>:</span><span style="color: #da8b55; ">38</span><span>: </span><span> line = python_file_bytes[</span><span style="color: #f2241f; ">current_idx</span><span>:linebreak_idx]
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">68</span><span>:</span><span style="color: #da8b55; ">50</span><span>: </span><span> line = python_file_bytes[current_idx:</span><span style="color: #f2241f; ">linebreak_idx</span><span>]
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">72</span><span>:</span><span style="color: #da8b55; ">12</span><span>: </span><span> if </span><span style="color: #f2241f; ">linebreak_idx</span><span> < 0:
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">75</span><span>:</span><span style="color: #da8b55; ">13</span><span>: </span><span> </span><span style="color: #f2241f; ">current_idx</span><span> = linebreak_idx + 1
</span><span style="color: #eb30ff; ">src/pyastgrep/files.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">75</span><span>:</span><span style="color: #da8b55; ">27</span><span>: </span><span> current_idx = </span><span style="color: #f2241f; ">linebreak_idx</span><span> + 1
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">244</span><span>:</span><span style="color: #da8b55; ">9</span><span>: </span><span> </span><span style="color: #f2241f; ">start_line_idx</span><span> = line_index - before_context
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">245</span><span>:</span><span style="color: #da8b55; ">9</span><span>: </span><span> </span><span style="color: #f2241f; ">end_line_idx</span><span> = line_index + after_context
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">246</span><span>:</span><span style="color: #da8b55; ">9</span><span>: </span><span> </span><span style="color: #f2241f; ">stop_line_idx</span><span> = end_line_idx + 1
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">246</span><span>:</span><span style="color: #da8b55; ">25</span><span>: </span><span> stop_line_idx = </span><span style="color: #f2241f; ">end_line_idx</span><span> + 1
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">248</span><span>:</span><span style="color: #da8b55; ">19</span><span>: </span><span> if (path, </span><span style="color: #f2241f; ">end_line_idx</span><span>) in self.printed_context_lines:
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">252</span><span>:</span><span style="color: #da8b55; ">19</span><span>: </span><span> if (path, </span><span style="color: #f2241f; ">start_line_idx</span><span> - 1) not in self.printed_context_lines:
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">253</span><span>:</span><span style="color: #da8b55; ">57</span><span>: </span><span> header = self.formatter.format_header(path, </span><span style="color: #f2241f; ">start_line_idx</span><span>)
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">257</span><span>:</span><span style="color: #da8b55; ">44</span><span>: </span><span> code = "\n".join(result.file_lines[</span><span style="color: #f2241f; ">start_line_idx</span><span>:stop_line_idx])
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">257</span><span>:</span><span style="color: #da8b55; ">59</span><span>: </span><span> code = "\n".join(result.file_lines[start_line_idx:</span><span style="color: #f2241f; ">stop_line_idx</span><span>])
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">260</span><span>:</span><span style="color: #da8b55; ">13</span><span>: </span><span> for </span><span style="color: #f2241f; ">idx</span><span> in range(start_line_idx, stop_line_idx):
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">260</span><span>:</span><span style="color: #da8b55; ">26</span><span>: </span><span> for idx in range(</span><span style="color: #f2241f; ">start_line_idx</span><span>, stop_line_idx):
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">260</span><span>:</span><span style="color: #da8b55; ">42</span><span>: </span><span> for idx in range(start_line_idx, </span><span style="color: #f2241f; ">stop_line_idx</span><span>):
</span><span style="color: #eb30ff; ">src/pyastgrep/printer.py</span><span>:</span><span style="color: #67b11d; font-weight: bold; ">261</span><span>:</span><span style="color: #da8b55; ">51</span><span>: </span><span> self.printed_context_lines.add((path, </span><span style="color: #f2241f; ">idx</span><span>))
</span></pre></section>
<section id="things-i-liked">
<h2><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-2" role="doc-backlink">Things I liked</a></h2>
<p>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.</p>
<p>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 <code class="docutils literal">Iterable[Match]</code> eventually all the way to <code class="docutils literal">Iterable[Match | MissingPath | ReadError | NonElementReturned | FileFinished]</code>. In each case, I could do something like:</p>
<ul class="simple">
<li><p>add the new return value, something like <code class="docutils literal">yield <span class="pre">MissingPath(...)</span></code>, in the body of the function.</p></li>
<li><p>fix up the function type signature, in response to the type error that mypy would now report.</p></li>
<li><p>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. <code class="docutils literal">isinstance</code> checks can be used to drive <a class="reference external" href="https://mypy.readthedocs.io/en/stable/type_narrowing.html">type narrowing</a> and satisfy mypy that everything is fine.</p></li>
</ul>
<p>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.</p>
<p>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.</p>
<p>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).</p>
<p>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.</p>
</section>
<section id="issues">
<h2><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-3" role="doc-backlink">Issues</a></h2>
<p>However, I have a long list of complaints about issues I found too!</p>
<section id="mypy-just-isnt-checking-that-code">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-4" role="doc-backlink">mypy just isn’t checking that code.</a></h3>
<p>You have to turn on at least:</p>
<div class="code"><pre class="code ini"><a id="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-1" name="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_4a2e4786fcd5416cb62b43b64fdb7777-1"></a><span class="na">check_untyped_defs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span><span class="w"></span>
<a id="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-2" name="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_4a2e4786fcd5416cb62b43b64fdb7777-2"></a><span class="na">disallow_untyped_calls</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span><span class="w"></span>
<a id="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-3" name="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_4a2e4786fcd5416cb62b43b64fdb7777-3"></a><span class="na">disallow_untyped_defs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span><span class="w"></span>
<a id="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-4" name="rest_code_4a2e4786fcd5416cb62b43b64fdb7777-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_4a2e4786fcd5416cb62b43b64fdb7777-4"></a><span class="na">disallow_incomplete_defs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span><span class="w"></span>
</pre></div>
<p>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.</p>
</section>
<section id="iobase-vs-binaryio">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-5" role="doc-backlink">IOBase vs BinaryIO</a></h3>
<p>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.</p>
<p>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 <code class="docutils literal">str</code> to <code class="docutils literal">bytes</code>. 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.</p>
<p>It turned out that according to mypy, <code class="docutils literal">IOBase.read()</code> returns <code class="docutils literal">Any</code>, and not the actual type <code class="docutils literal">bytes</code> or <code class="docutils literal">str</code>. I had been using <code class="docutils literal">IOBase</code> as a type for some of my arguments, which meant that mypy didn’t pick up the problem – if <code class="docutils literal">Any</code> appears anywhere, it’s like throwing a “silently disable everything this touches” bomb into the type checker.</p>
<p>Now, I had been alerted to the problem earlier – mypy thinks that <code class="docutils literal">sys.stdin</code> is of type <code class="docutils literal">typing.TextIO</code>, not <code class="docutils literal">IOBase</code>. However, <code class="docutils literal">typing.TextIO</code> is not something you test for at run-time, so it interacts badly with the very useful <code class="docutils literal">isinstance</code> type guards and type narrowing. So I had ended up using <code class="docutils literal">IOBase</code> as that seemed less problematic.</p>
<p>In other words, I <strong>had</strong> added type hints, and had I added <strong>correct</strong> type hints. But they weren’t correct <strong>enough</strong>, 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.</p>
<p>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.</p>
<p>It involved a long journey of understanding regarding type guards and <a class="reference external" href="https://peps.python.org/pep-0724/">stricter type guards</a>, because non-strict type guards (which is what we have at the moment) <a class="reference external" href="https://github.com/python/mypy/issues/13957">turned out not to work how I thought</a>. The eventual refactoring now uses the <code class="docutils literal">typing.BinaryIO</code> 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 <code class="docutils literal">BinaryIO</code>, I have to order my if/elif/else clauses in exactly the right way.</p>
<p>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.</p>
</section>
<section id="missing-types-for-imports">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-6" role="doc-backlink">Missing types for imports</a></h3>
<p>I saw a bug I hadn’t fixed, and one that again I thought mypy might have caught.</p>
<p>It turns out I had added <code class="docutils literal">ignore_missing_imports = true</code> early in development to reduce the errors to a manageable set. This silenced errors relating to lxml and effectively gave me a bunch of <code class="docutils literal">Any</code> types floating around instead of something useful.</p>
<p>Again, this was “my fault”, but I feel it’s fairly typical of what will happen in the real world. Switching to <code class="docutils literal">ignore_missing_imports = false</code> 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.</p>
</section>
<section id="equality-checks">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-7" role="doc-backlink">Equality checks</a></h3>
<p>I switched a bunch of code paths from <code class="docutils literal">str</code> to <code class="docutils literal">Path</code> at one point. mypy gave me some help, but less than I wanted. For example, this reports no error:</p>
<div class="code"><pre class="code python"><a id="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-1" name="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-1"></a><span class="n">path</span><span class="p">:</span> <span class="n">Path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<a id="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-2" name="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-2"></a>
<a id="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-3" name="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-3"></a><span class="k">if</span> <span class="n">path</span> <span class="o">==</span> <span class="s2">"-"</span><span class="p">:</span>
<a id="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-4" name="rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_8c0b6e91b03b4828b57313eff2cf8ec7-4"></a> <span class="o">...</span>
</pre></div>
<p>A comparison between a <code class="docutils literal">str</code> and a <code class="docutils literal">Path</code> always returns <code class="docutils literal">False</code>, so it’s not a useful thing to do, and therefore a developer error. I meant to do the comparison to <code class="docutils literal"><span class="pre">"-"</span></code> before I converted the input <code class="docutils literal">str</code> objects to internal <code class="docutils literal">Path</code> objects. It’s conceptually a <code class="docutils literal">TypeError</code>, but not actually one. Thankfully I had tests that failed.</p>
</section>
<section id="mypy-caching-bugs">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-8" role="doc-backlink">mypy caching bugs</a></h3>
<p>Several times I had to blow away <code class="docutils literal">.mypy_cache</code> 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 <strong>knew</strong> 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”.</p>
</section>
<section id="third-party-types">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-9" role="doc-backlink">Third party types</a></h3>
<p>Are types provided by third party libs or typeshed reliable?</p>
<p>No, they are not. For example, I discovered this one in <a class="reference external" href="https://github.com/python/typeshed/blob/a094aa09c2aa47721664d3fdef91eda4fac24ebb/stdlib/_ast.pyi#L19">typeshed/stdlib/_ast.pyi</a> among many others:</p>
<div class="code"><pre class="code python"><a id="rest_code_5c380eaab43d4b878a1a1aba4137e6dc-1" name="rest_code_5c380eaab43d4b878a1a1aba4137e6dc-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_5c380eaab43d4b878a1a1aba4137e6dc-1"></a><span class="k">class</span> <span class="nc">AST</span><span class="p">:</span>
<a id="rest_code_5c380eaab43d4b878a1a1aba4137e6dc-2" name="rest_code_5c380eaab43d4b878a1a1aba4137e6dc-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_5c380eaab43d4b878a1a1aba4137e6dc-2"></a> <span class="o">...</span>
<a id="rest_code_5c380eaab43d4b878a1a1aba4137e6dc-3" name="rest_code_5c380eaab43d4b878a1a1aba4137e6dc-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_5c380eaab43d4b878a1a1aba4137e6dc-3"></a> <span class="c1"># TODO: Not all nodes have all of the following attributes</span>
</pre></div>
<p>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.</p>
</section>
<section id="duck-typing">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-10" role="doc-backlink">Duck typing</a></h3>
<p>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 <a class="reference external" href="https://docs.python.org/3/library/typing.html#typing.Protocol">Protocol</a>, but I’m simply noting you do have significant amounts of extra work for the type checker to understand idiomatic Python.</p>
</section>
<section id="false-security">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-11" role="doc-backlink">False security</a></h3>
<p>I fairly often got that sense of “it type checks, and everything works first time I run it, cool!”</p>
<p>Sometimes, it was an illusion though – take this code:</p>
<div class="code"><pre class="code python"><a id="rest_code_6bf898ff493c4551b13624ce9e746f5c-1" name="rest_code_6bf898ff493c4551b13624ce9e746f5c-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_6bf898ff493c4551b13624ce9e746f5c-1"></a><span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">color</span> <span class="o">==</span> <span class="n">UseColor</span><span class="o">.</span><span class="n">AUTO</span><span class="p">:</span>
<a id="rest_code_6bf898ff493c4551b13624ce9e746f5c-2" name="rest_code_6bf898ff493c4551b13624ce9e746f5c-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_6bf898ff493c4551b13624ce9e746f5c-2"></a> <span class="n">colorer</span> <span class="o">=</span> <span class="n">make_default_colorer</span><span class="p">()</span>
<a id="rest_code_6bf898ff493c4551b13624ce9e746f5c-3" name="rest_code_6bf898ff493c4551b13624ce9e746f5c-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_6bf898ff493c4551b13624ce9e746f5c-3"></a><span class="k">elif</span> <span class="n">args</span><span class="o">.</span><span class="n">colors</span> <span class="o">==</span> <span class="n">UseColor</span><span class="o">.</span><span class="n">NEVER</span><span class="p">:</span>
<a id="rest_code_6bf898ff493c4551b13624ce9e746f5c-4" name="rest_code_6bf898ff493c4551b13624ce9e746f5c-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_6bf898ff493c4551b13624ce9e746f5c-4"></a> <span class="n">colorer</span> <span class="o">=</span> <span class="n">NullColorer</span><span class="p">()</span>
</pre></div>
<p>I had typed <code class="docutils literal">colors</code> instead of the correct <code class="docutils literal">color</code> in the second branch, but I got no squiggly red lines. This was because of a lurking <code class="docutils literal">Any</code> – the argparse <code class="docutils literal">args</code> object is actually an <code class="docutils literal">Any</code>. This tripped me up, because I didn’t have any tests for that line of code.</p>
<p>Having <code class="docutils literal">strict = true</code> in your mypy config doesn’t fix this. I think I’d need a way to say “warn me for about anywhere that <code class="docutils literal">Any</code> leaks into my code base”, but even if it existed I imagine I would not like it.</p>
</section>
<section id="exhaustiveness-checking">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-12" role="doc-backlink">Exhaustiveness checking</a></h3>
<p>mypy fails to find the obvious issue in this bit of code:</p>
<div class="code"><pre class="code python"><a id="rest_code_b1aad54a40c349f0b044cbdd51a6b585-1" name="rest_code_b1aad54a40c349f0b044cbdd51a6b585-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_b1aad54a40c349f0b044cbdd51a6b585-1"></a><span class="k">def</span> <span class="nf">foo</span><span class="p">()</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
<a id="rest_code_b1aad54a40c349f0b044cbdd51a6b585-2" name="rest_code_b1aad54a40c349f0b044cbdd51a6b585-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_b1aad54a40c349f0b044cbdd51a6b585-2"></a> <span class="k">if</span> <span class="mi">1</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
<a id="rest_code_b1aad54a40c349f0b044cbdd51a6b585-3" name="rest_code_b1aad54a40c349f0b044cbdd51a6b585-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_b1aad54a40c349f0b044cbdd51a6b585-3"></a> <span class="n">x</span> <span class="o">=</span> <span class="s2">"hello"</span>
<a id="rest_code_b1aad54a40c349f0b044cbdd51a6b585-4" name="rest_code_b1aad54a40c349f0b044cbdd51a6b585-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_b1aad54a40c349f0b044cbdd51a6b585-4"></a> <span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</pre></div>
<p>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:</p>
<div class="code"><pre class="code python"><a id="rest_code_345f50377f1a435c847407d55b46d8b4-1" name="rest_code_345f50377f1a435c847407d55b46d8b4-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_345f50377f1a435c847407d55b46d8b4-1"></a><span class="k">def</span> <span class="nf">print_greeting</span><span class="p">(</span><span class="n">username</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="nb">type</span><span class="p">:</span> <span class="n">Greeting</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
<a id="rest_code_345f50377f1a435c847407d55b46d8b4-2" name="rest_code_345f50377f1a435c847407d55b46d8b4-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_345f50377f1a435c847407d55b46d8b4-2"></a> <span class="k">if</span> <span class="nb">type</span> <span class="o">==</span> <span class="n">Greeting</span><span class="o">.</span><span class="n">HELLO</span><span class="p">:</span>
<a id="rest_code_345f50377f1a435c847407d55b46d8b4-3" name="rest_code_345f50377f1a435c847407d55b46d8b4-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_345f50377f1a435c847407d55b46d8b4-3"></a> <span class="n">greeting</span> <span class="o">=</span> <span class="s2">"hello"</span>
<a id="rest_code_345f50377f1a435c847407d55b46d8b4-4" name="rest_code_345f50377f1a435c847407d55b46d8b4-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_345f50377f1a435c847407d55b46d8b4-4"></a> <span class="k">elif</span> <span class="nb">type</span> <span class="o">==</span> <span class="n">Greeting</span><span class="o">.</span><span class="n">GOODBYE</span><span class="p">:</span>
<a id="rest_code_345f50377f1a435c847407d55b46d8b4-5" name="rest_code_345f50377f1a435c847407d55b46d8b4-5" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_345f50377f1a435c847407d55b46d8b4-5"></a> <span class="n">greeting</span> <span class="o">=</span> <span class="s2">"goodbye"</span>
<a id="rest_code_345f50377f1a435c847407d55b46d8b4-6" name="rest_code_345f50377f1a435c847407d55b46d8b4-6" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_345f50377f1a435c847407d55b46d8b4-6"></a>
<a id="rest_code_345f50377f1a435c847407d55b46d8b4-7" name="rest_code_345f50377f1a435c847407d55b46d8b4-7" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_345f50377f1a435c847407d55b46d8b4-7"></a> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">greeting</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</pre></div>
<p>You can get this right using an <code class="docutils literal">else</code> branch with <a class="reference external" href="https://typing.readthedocs.io/en/latest/source/unreachable.html">assert_never</a>, but it’s annoying that you have to remember to do this.</p>
</section>
<section id="decorators">
<h3><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-13" role="doc-backlink">Decorators</a></h3>
<p><a class="reference external" href="https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators">Type hints for decorators</a> are … bad. If you want parameterised decorators, or other people’s decorators that don’t have types</p>
<p>[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]</p>
</section>
</section>
<section id="pyright">
<h2><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-14" role="doc-backlink">pyright</a></h2>
<p>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.</p>
<p>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 <strong>without</strong> needing the <code class="docutils literal">assert_never</code> 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:</p>
<div class="code"><pre class="code python"><a id="rest_code_652ff8e9800f45b38044cdcd76725632-1" name="rest_code_652ff8e9800f45b38044cdcd76725632-1" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_652ff8e9800f45b38044cdcd76725632-1"></a><span class="k">try</span><span class="p">:</span>
<a id="rest_code_652ff8e9800f45b38044cdcd76725632-2" name="rest_code_652ff8e9800f45b38044cdcd76725632-2" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_652ff8e9800f45b38044cdcd76725632-2"></a> <span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">StrEnum</span>
<a id="rest_code_652ff8e9800f45b38044cdcd76725632-3" name="rest_code_652ff8e9800f45b38044cdcd76725632-3" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_652ff8e9800f45b38044cdcd76725632-3"></a><span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
<a id="rest_code_652ff8e9800f45b38044cdcd76725632-4" name="rest_code_652ff8e9800f45b38044cdcd76725632-4" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#rest_code_652ff8e9800f45b38044cdcd76725632-4"></a> <span class="kn">from</span> <span class="nn">backports.strenum</span> <span class="kn">import</span> <span class="n">StrEnum</span> <span class="c1"># type: ignore [no-redef]</span>
</pre></div>
<p>I eventually <a class="reference external" href="https://github.com/microsoft/pyright/issues/4076">found an adequate solution</a> 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”.</p>
<p>In addition, in some places pyright does <strong>not</strong> support the <code class="docutils literal">assert_never</code> technique that mypy needs, and reports an error. There are <a class="reference external" href="https://github.com/microsoft/pyright/issues/4706">other pain points</a> if you try to use both.</p>
<p>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 <a class="reference external" href="https://github.com/microsoft/pyright/issues?q=is%3Aissue+is%3Aclosed+label%3A%22as+designed%22">large numbers of issues closed with the “as designed” tag</a> to find answers.</p>
</section>
<section id="summary">
<h2><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-15" role="doc-backlink">Summary</a></h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>In terms of tips and advice:</p>
<ul class="simple">
<li><p>You need to turn up error reporting and <a class="reference external" href="https://rtpg.co/2023/03/07/how-to-adopt-mypy-on-bigger-projects.html">spend considerable effort configuring mypy</a>, especially in larger projects.</p></li>
<li><p>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:</p>
<ul>
<li><p>probably not much in stdlib. You’re going to need to wrap everything.</p></li>
<li><p>probably not much that was created more than 5 years ago.</p></li>
</ul>
</li>
</ul>
<p>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.</p>
</section>
<hr class="docutils">
<section id="links">
<h2><a class="toc-backref" href="https://lukeplant.me.uk/blog/posts/python-type-hints-pyastgrep-case-study/#toc-entry-16" role="doc-backlink">Links</a></h2>
<ul class="simple">
<li><p><a class="reference external" href="https://lobste.rs/s/ecmsdo/python_type_hints_pyastgrep_case_study">Discussion of this post on Lobsters</a></p></li>
</ul>
</section>Super-fast Sphinx docs, and SNOB driven developmenthttps://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/2023-09-27T15:05:48+01:002023-09-27T15:05:48+01:00Luke Plant<p>Code that will make your static doc pages seriously faster, that you seriously don’t need</p><p>If you are using static HTML files for your docs, such as with <a class="reference external" href="https://www.sphinx-doc.org/en/master/">Sphinx</a> or many other doc generators, here is a chunk of code that will speed up loading of pages after the first one. If you’re using some other docs generator, the instructions will probably work with minimal adaptation.</p>
<ol class="arabic">
<li><p>Create a <code class="docutils literal">custom.js</code> file inside your <code class="docutils literal">_static</code> directory, with the following contents:</p>
<div class="code"><pre class="code javascript"><a id="rest_code_d64135d7232c4544accf5c37446f0fb0-1" name="rest_code_d64135d7232c4544accf5c37446f0fb0-1" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-1"></a><span class="kd">var</span><span class="w"> </span><span class="nx">script</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'script'</span><span class="p">);</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-2" name="rest_code_d64135d7232c4544accf5c37446f0fb0-2" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-2"></a><span class="nx">script</span><span class="p">.</span><span class="nx">src</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://unpkg.com/htmx.org@1.9.5"</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-3" name="rest_code_d64135d7232c4544accf5c37446f0fb0-3" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-3"></a><span class="nx">script</span><span class="p">.</span><span class="nx">integrity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"sha384-xcuj3WpfgjlKF+FXhSQFQ0ZNr39ln+hwjN3npfM9VBnUskLolQAcN80McRIVOPuO"</span><span class="p">;</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-4" name="rest_code_d64135d7232c4544accf5c37446f0fb0-4" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-4"></a><span class="nx">script</span><span class="p">.</span><span class="nx">crossOrigin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'anonymous'</span><span class="p">;</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-5" name="rest_code_d64135d7232c4544accf5c37446f0fb0-5" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-5"></a><span class="nx">script</span><span class="p">.</span><span class="nx">onload</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-6" name="rest_code_d64135d7232c4544accf5c37446f0fb0-6" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-6"></a><span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s2">"body"</span><span class="p">);</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-7" name="rest_code_d64135d7232c4544accf5c37446f0fb0-7" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-7"></a><span class="w"> </span><span class="nx">body</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'hx-boost'</span><span class="p">,</span><span class="w"> </span><span class="s2">"true"</span><span class="p">);</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-8" name="rest_code_d64135d7232c4544accf5c37446f0fb0-8" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-8"></a><span class="w"> </span><span class="nx">htmx</span><span class="p">.</span><span class="nx">process</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-9" name="rest_code_d64135d7232c4544accf5c37446f0fb0-9" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-9"></a><span class="p">}</span><span class="w"></span>
<a id="rest_code_d64135d7232c4544accf5c37446f0fb0-10" name="rest_code_d64135d7232c4544accf5c37446f0fb0-10" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_d64135d7232c4544accf5c37446f0fb0-10"></a><span class="nb">document</span><span class="p">.</span><span class="nx">head</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">script</span><span class="p">);</span><span class="w"></span>
</pre></div>
</li>
<li><p>Add an item to your <code class="docutils literal">html_js_files</code> setting in your Sphinx <code class="docutils literal">conf.py</code>:</p>
<div class="code"><pre class="code python"><a id="rest_code_536742c61d14439b80576e2e0c694e09-1" name="rest_code_536742c61d14439b80576e2e0c694e09-1" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_536742c61d14439b80576e2e0c694e09-1"></a><span class="n">html_js_files</span> <span class="o">=</span> <span class="p">[</span>
<a id="rest_code_536742c61d14439b80576e2e0c694e09-2" name="rest_code_536742c61d14439b80576e2e0c694e09-2" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_536742c61d14439b80576e2e0c694e09-2"></a> <span class="s1">'custom.js'</span><span class="p">,</span>
<a id="rest_code_536742c61d14439b80576e2e0c694e09-3" name="rest_code_536742c61d14439b80576e2e0c694e09-3" href="https://lukeplant.me.uk/blog/posts/super-fast-sphinx-docs/#rest_code_536742c61d14439b80576e2e0c694e09-3"></a><span class="p">]</span>
</pre></div>
</li>
</ol>
<p>Rebuild and you’re done.</p>
<p>What this script does is:</p>
<ol class="arabic simple">
<li><p>Load the <a class="reference external" href="https://htmx.org/">htmx</a> library.</p></li>
<li><p>If it successfully loads, adds the <a class="reference external" href="https://htmx.org/attributes/hx-boost/">hx-boost</a> attribute to the body element.</p></li>
<li><p>Initialises htmx on the page.</p></li>
</ol>
<p>This means that htmx will intercept all internal links on the page, and instead of letting the browser load them the normal way, it sends an AJAX request and swaps in the content of the page. This means that the whole page doesn’t need to be reloaded by the browser, saving precious milliseconds.</p>
<section id="actually-please-dont">
<h2>Actually, please don’t</h2>
<p>I will provide reasons why you really shouldn’t use the code above, although it works almost perfectly. But first, a rant.</p>
<p>This post was inspired by <a class="reference external" href="https://www.mux.com/blog/what-are-react-server-components">Mux’s blog post on migrating 50,000 lines of React Server Components</a>. It contains a nice overview of the history of web site architecture, including this quote:</p>
<blockquote>
<p>Then, we started wondering: What if we wanted faster responses and more interactivity? Every time a user takes an action, do we really want to send cookies back to the server and make the server generate a whole new page? What if we made the client do that work instead? We can just send all the rendering code to the client as JavaScript!</p>
<p>This was called client-side rendering (CSR) or single-page applications (SPA) and was <a class="reference external" href="https://begin.com/blog/posts/2023-02-21-why-does-everyone-suddenly-hate-single-page-apps">widely considered a bad move</a></p>
</blockquote>
<p>However, instead of then suggesting that we perhaps we should retrace our steps, the article just plunges on and on, deeper and deeper into the jungle.</p>
<p>Now, this might all make sense if we are talking about a highly interactive site that has the highest possible needs in terms of user interactivity. But I realised the article was about <strong>just their documentation site</strong>, not the main application.</p>
<p>Now, some docs sites are really fancy and do very clever interactive things. Mux’s, however, is not like that. The only interactive things I could find were:</p>
<ul class="simple">
<li><p>tabs – like you can get with something like <a class="reference external" href="https://sphinx-code-tabs.readthedocs.io/en/latest/">sphinx-code-tabs</a>, powered by a <a class="reference external" href="https://sphinx-code-tabs.readthedocs.io/en/latest/_static/code-tabs.js">tiny bit of Javascript</a>.</p></li>
<li><p>their <a class="reference external" href="https://docs.mux.com/changelog">changelog page</a> – which is more complicated, but whose essential functionality could again be implemented by a really small amount of Javascript added to a static page. I should also note that their page is really pretty slugish when you change the filters, much slower than you would get by an approach that just selectively hides parts of the page using DOM manipulation.</p></li>
<li><p>search. Search is definitely important, but I can’t see why it means the whole site needs to be implemented in React.</p></li>
<li><p>A “Was this helpful” component – this could have been a small web component or something similar.</p></li>
<li><p>A few fancy transitions in the side bar.</p></li>
</ul>
<p>These are not the highly stateful pages that React was designed for. Maybe there are a few other things I didn’t find, but 95% of it could be handled using entirely static HTML, built by any number of simple docs generators, with tiny amounts of Javascript.</p>
<p>The only other thing I noticed is that page transitions generally had that instant feel an SPA can give you, and were noticeably faster than you would get with the static HTML solution I’m suggesting.</p>
<p>So, not to be beaten, I came up with the above solution on htmx so I could match the speed.</p>
<p>Now, here’s why you shouldn’t use it:</p>
<ul>
<li><p>A typical docs page with Sphinx loads in a few hundred milliseconds, which is fine. Do you really need to shave that down to less than 50 so it feels “instant”? Do your users care?</p></li>
<li><p>While it is truly a tiny fraction of the complexity of the React docs site Mux described in their post, you are still adding some significant complexity. Is it worth is?</p></li>
<li><p>Are you sure it’s not going to interact badly with some Javascript on some page, maybe some future Javascript you will add?</p></li>
<li><p>Have you considered all use cases – like the person who downloads your whole docs site using <code class="docutils literal">wget <span class="pre">--recursive</span></code> so they can browse offline? Answer: if they have no internet connection when they view the docs, it will actually work fine, because the htmx library won’t load at all. But if they are online, the htmx library will load, and then every internal link will break due to <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS errors</a>. <strong>You just broke offline viewing</strong>. You could fix this very easily with an extra conditional in the script above, but I’m making a point. Is there anything else that’s broken?</p>
<p>No prizes for guessing that while Sphinx-generated sites normally work perfectly with <code class="docutils literal">wget <span class="pre">--recursive</span></code> for offline viewing, docs.mux.com does not work well, to put it mildly. I also wasted hundreds of Mb finding out, due to the vast amount of boilerplate every single HTML file has. Don’t be like them.</p>
</li>
</ul>
<p>This is what you should actually do:</p>
<ul class="simple">
<li><p>recognise that you know exactly how to make your documentation pages load instantly, like an SPA, and could absolutely do it if you wanted to, still with a tiny fraction of the complexity of an actual SPA architecture, and with fixes for the issues I’ve mentioned, in about 15 minutes, then,</p></li>
<li><p>don’t.</p></li>
</ul>
<p>As protection against the FOMO and fashion that drives so much of web development, this attitude needs a catchy slogan, which is the kind of thing I’m not very good at. But as a first attempt, how about: SNOB driven development. SNOB means “Smugly kNOwing Better”. Or maybe that could be “Smugly NO-ing Better”.</p>
<p>Join me. Be an arrogant SNOB and just say No.</p>
</section>Is the Lord's Day the Christian Sabbath?https://lukeplant.me.uk/blog/posts/is-the-lords-day-the-christian-sabbath/2023-08-24T10:15:37+01:002023-08-24T10:15:37+01:00Luke Plant<p>Most of this post was written a long time ago, but I thought it would be useful to have somewhere public that I can point people to for my answer to this question, so I’m finally publishing it.</p>
<p>At the outset, I need to say that this issue is one that I think Christians should not divide over. The view I present below is not the one I grew up with, but I have no particular ambition to convert people to my view — except that, with regard to those who have the duty to teach God’s word, it is important to do so properly, “rightly handling the word of truth”, preaching the full counsel of God with all His authority, but never giving human ideas that same authority. It is to people with those duties that the following is really directed. The tone of this article should be interpreted with that in mind — my concern is with those who are not rightly teaching scripture (while being aware that I have failed and probably continue to fail in this extremely demanding privilege in many ways).</p>
<p>Before going on — if you are worried about the length of this article, the last two thirds of it actually consists of an appendix containing quotations from the early church, and are not part of the main argument.</p>
<section id="definitions">
<h2>Definitions</h2>
<p>For my definition of the concept of a “Christian Sabbath” or “Christian Sabbatarianism”, I will take this quotation from The Westminster Confession:</p>
<blockquote>
<p>Chapter 21 VII. As it is of the law of nature, that, in general, a due proportion of time be set apart for the worship of God; so, in his Word, by a positive, moral, and perpetual commandment, binding all men in all ages, he hath particularly appointed one day in seven for a Sabbath, to be kept holy unto him: which, from the beginning of the world to the resurrection of Christ, was the last day of the week; and, from the resurrection of Christ, was changed into the first day of the week, which in Scripture is called the Lord’s Day, and is to be continued to the end of the world as the Christian Sabbath.</p>
</blockquote>
</section>
<section id="principles">
<h2>Principles</h2>
<p>We need to ask if the above statement is biblically grounded or not.</p>
<p>First, a principle: in teaching people to obey God, it is a sin to add to the commands that God has given us. We are allowed to go no further than what the Bible itself requires in the demands we place on people, or we come under the condemnation of Jesus (Matthew 15:7-9).</p>
<p>We must teach only what the Bible teaches, and what can and must be deduced from it. As the Westminster Confession puts it so well:</p>
<blockquote>
<p>Chapter 1. VI. The whole counsel of God, concerning all things necessary for his own glory, man's salvation, faith, and life, is either expressly set down in Scripture, or by good and necessary consequence may be deduced from Scripture: unto which nothing at any time is to be added, whether by new revelations of the Spirit, or traditions of men.</p>
</blockquote>
<p>In other words, we are not free to extrapolate, “read between the lines” or “join up the dots” in any way we please, but must teach all of what Scripture explicitly says and what <strong>necessarily</strong> flows from it, according to its own logic, and only that.</p>
<p>We note that Scripture may teach by precept, example or implication, but precept is stronger than example, as an example of behaviour found in the Bible could be good, bad, or incidental. Implication can be fairly strong or fairly weak, depending on the details.</p>
</section>
<section id="questions">
<h2>Questions</h2>
<p>I will respond to the claims of the Westminster Confession with a series of questions:</p>
<ol class="arabic">
<li><p>Does the NT ever refer to the Lord’s Day as the Sabbath?</p>
<p>No, it does not.</p>
<p>This is already sufficient reason to not call the Lord’s Day the Sabbath. If the Bible doesn't call it that, it may well have good reasons for not doing so, and we will only succeed in confusing ourselves and biasing our reading of Scripture when we use biblical terminology in unbiblical ways.</p>
<p>To give an illustration:</p>
<p>In charismatic and Pentecostal circles, it is quite common to use the phrase “waiting on the Lord” to mean a kind of meditative, “listening” prayer in which you wait for the Holy Spirit to lead your thoughts directly, and interpret that as the voice of God.</p>
<p>One of the difficulties with this is that, in my view, it is taking a biblical phrase and using it in an unbiblical way — I think “waiting on/for the Lord” in the Bible is actually about trusting God. This produces a feedback loop that is difficult to escape from. Because of how the phrase is used in those circles, every time they read Psalm 130:5,6, Isaiah 40:31 or similar passages, it is firstly assumed that the Bible is talking about their practice of listening in prayer. Those texts then reinforce not just the legitimacy of the practice, but its importance.</p>
<p>When asked for biblical support for their practice, they do point to these texts — despite the fact that the phrases they contain have been interpreted according to their usage of that terminology, rather than actually describing the practice in a clear way. It becomes very difficult for them to believe that listening in prayer is either unbiblical or not as important as they have thought — after all, they know for a fact that they've been encouraged to do that many times in God’s word, even if they can't remember where.</p>
<p>(I’m not saying here that God never leads us via our thoughts when praying, by the way, that’s another issue I’m not getting in to.)</p>
<p>In the same way, if we call the Lord’s Day “the Sabbath”, every time we read the Ten Commandments or many other passages about the Sabbath, we equate “Sabbath” with “the Lord’s Day”, creating a feedback loop that makes it very difficult to even take the non-Sabbatarian view seriously — after all, we know for a fact that God has told us that it is a sin to work on the Lord’s Day, being unaware of the unbiblical interpretative jump our minds have made. I suspect that this is the primary reason that the Sabbatarian position retains a hold over many Christian circles.</p>
<p>And, by the way, as far as I can tell from the records we have, in at least the first 4 centuries, while Christian teachers often mentioned “the Sabbath”, they never used that word to refer to the Lord's Day — see Appendix.</p>
</li>
<li><p>Does the NT ever prescriptively take Sabbath laws and apply them to the Lord’s day (e.g. command people not to work on the Lord’s Day)?</p>
<p>No, it does not.</p>
</li>
<li><p>Does the NT ever descriptively set out a pattern of Christians observing Sabbath regulations on the Lord’s day?</p>
<p>No. We do find Christians worshipping God on the first day of the week. But they worshipped on other days too (<a class="reference external" href="https://www.biblegateway.com/passage/?search=Acts%202%3A46&version=NIVUK">Acts 2:46</a>). Whether Christians are required to spend some time worshipping God on the Lord’s Day is a different question to whether the Lord’s Day is the Sabbath. We are certainly never told they avoided labour or recreation on the Lord’s Day, or gave the whole day over to the worship of God.</p>
<p>In <a class="reference external" href="https://www.biblegateway.com/passage/?search=Acts+20%3A7&version=NIVUK">Acts 20:7</a>, the disciples there apparently met late at night.</p>
<p>Often it seems they met before dawn on Sunday:</p>
<blockquote>
<p>They asserted, however, that the sum and substance of their fault or error had been that they were accustomed to meet on a fixed day before dawn and sing responsively a hymn to Christ as a god</p>
<p class="attribution">—Pliny</p>
</blockquote>
<p>Most likely, the unsociable hours of these meetings were due to the fact that they were working the rest of the time, since Sunday was an official day of work in the Roman Empire until Constantine.</p>
<p>The following is an argument from silence, and is therefore weaker, but I think it has some strength to it:</p>
<p>Had the early church been refusing to work on Sunday, this would have been scandalous, and a more than adequate justification for persecution (at least in the eyes of the persecutors). It seems fairly unlikely historically that if Christians had the practice of taking the whole of Sunday off, that there would be no record of it, especially given documents like Justin Martyr's Apologies, where he defends Christians against the grievances that others had against them.</p>
</li>
</ol>
<p>This leaves the case for Christian Sabbatarianism on very shaky ground, with neither precept nor example to support it.</p>
<p>However, we still want to answer the question “what should we do with the 4th commandment?”. This could potentially provide a case for a Christian Sabbath concept by way of some biblical logic. Answering this question requires looking at both the OT background to the command, and how the NT treats it.</p>
</section>
<section id="old-testament-treatment-of-the-sabbath">
<h2>Old Testament treatment of the Sabbath</h2>
<p>We find:</p>
<ol class="arabic simple">
<li><p>The Sabbath is not a creation ordinance, despite what some say. Adam was commanded to work, reproduce etc., but there is no command to rest every seven days. The account of God’s creation in six days and rest on the seventh is not made into any kind of pattern in the book of Genesis, and while Genesis 2:3 talks of God blessing the seventh day and making it holy, it doesn’t fill out what that means in terms of a requirement not to work.</p></li>
<li><p>There is no record of anyone observing Sabbaths until we come to Moses. (See also quotes from Justyn Martyr and Tertullian below, who said that Abel, Enoch, Noah and Melchizedek did not observe Sabbaths).</p></li>
<li><p>The creation-basis for the command in the law of Moses is not a strict copy, but an adaptation based on the pattern. God worked for 6 days, then had an eternal day of rest (there is no “evening and morning the seventh day”). This is then adapted into a weekly cycle with a commandment to cease from labour for the Jews. So we are primed for the idea that the creation principle of rest may be adapted in different ways in the New Covenant.</p></li>
<li><p>The Jewish Sabbath is a special sign of God’s covenant with the Jews — see <a class="reference external" href="Ezekiel20:11-12">Ezekiel 20:11-12</a>. Clearly God couldn't have said this of moral laws e.g. the command not to murder could not have been called a “sign” between God and the Jews, since it was common to Jews and the rest of the world.</p></li>
</ol>
<p>On this basis, it seems very unlikely that the Jewish Sabbath is part of the moral law that all the world must obey. The Westminster Confession does not have an adequate biblical basis for saying that God appointed one day in seven to be kept holy by all people “from the beginning of the world”.</p>
</section>
<section id="new-testament-treatment-of-the-sabbath">
<h2>New Testament treatment of the Sabbath</h2>
<p>Does the NT ever speak directly on the issue of how laws about Sabbath or special day observance are to be handled by Christians? Thankfully, it does:</p>
<ol class="arabic">
<li><p><a class="reference external" href="https://www.biblegateway.com/passage/?search=Colossians%202%3A16-17&version=NIVUK">Colossians 2:16-17</a>, <a class="reference external" href="https://www.biblegateway.com/passage/?search=Galatians%204%3A10&version=NIVUK">Galatians 4:10</a>, <a class="reference external" href="https://www.biblegateway.com/passage/?search=Romans%2014%3A5&version=NIVUK">Romans 14:5</a>.</p>
<p>These texts are clear, and do not require exegetical somersaults to understand, once Sabbatarian glasses have been removed:</p>
<ul class="simple">
<li><p>The Sabbath is, like other OT ceremonies, a shadow that is fulfilled in Christ.</p></li>
<li><p>We are at liberty to observe special days if we want to, but not to require other people to do so.</p></li>
</ul>
<p>In the NT, there are no holy things or places or days, only a holy people.</p>
</li>
<li><p><a class="reference external" href="https://www.biblegateway.com/passage/?search=Hebrews%204%3A1-11&version=NIVUK">Hebrews 4:1-11</a>.</p>
<p>The Sabbath is fulfilled for Christians by the eternal rest of the new heavens and new earth. Christians enter that now in spiritual ways, by resting in Christ, and ceasing their attempts to gain acceptance by works.</p>
</li>
</ol>
<p>Negatively, the council of Jerusalem is also deafening by its silence on the issue. It specifically discusses the points where Jewish law impinged upon Gentile consciences. Sabbath observance was not practised among Gentiles, so I think the silence of Acts 15 on this matter is rather difficult to explain if the apostles believed that Sabbath observance was necessary for Gentiles and had been moved to the Lord’s Day after Christ’s resurrection, as claimed by the Westminster Confession.</p>
<p>OT and NT point unambiguously in the same direction. Other texts that are sometimes quoted (“The Son of Man is Lord of the Sabbath” etc.) simply don't address the question (unless you have made the unbiblical equation of “the Sabbath” equals “the Lord’s Day”, in which case seeing these texts clearly will require a fair amount of un-thinking).</p>
<p>One text which is sometimes used to support the universality of the Sabbath is Mark 2:27, “The Sabbath was made for man, not man for the Sabbath”. This argument depends firstly on translating ‘anthropos’ as ‘man’, and then understanding ‘man’ to mean ‘all mankind’. It is perfectly possible to translate ‘anthropos’ as ‘people’ (as per <a class="reference external" href="https://www.biblegateway.com/passage/?search=Mark%202%3A27&version=NET">the NET translation</a>) or understand it in that way, and the argument then disappears. If I said “The Highway Code was made for people, not people for the Highway Code”, I would not be implying that wherever there are people, there is the Highway Code, and it will always be that way, time without end. In fact I would more likely be implying the opposite — the Highway Code is an invention that serves human needs, and can be adjusted or abandoned if necessary. The question is then: which meaning is more appropriate for this text? Given the OT history, which gives no hint of Sabbath observance for all mankind, either by precept or example, with the Sabbath being introduced by Moses and understood as a specific sign of God’s covenant with the nation of Israel (as above), it seems far more appropriate to understand this text as meaning simply “the Sabbath was made for people” — and not as a statement of the universality of the Sabbath.</p>
<p>There remains one argument I know of in favour of Christian Sabbatarianism: the Sabbath is part of the Ten Commandments, which are God’s eternal moral law, and therefore must continue.</p>
<p>First, in response, the Bible never states that the Ten Commandments are God’s eternal moral law. The division of the law into moral, civil and ceremonial, while useful, is not strictly biblical, and must always be subject to what the Bible actually says. The NT texts on the Sabbath make it clear that the Jewish form of the Sabbath (one day in seven rest) is ceremonial. We must not allow the systems that we have extracted from scripture (or think we have) to override plain exegesis. It is infinitely better to have holes, even gaping holes, in our systematic theology, than to handle the Bible in such a way that we override or ignore just one of God's holy words.</p>
<p>The argument that the Sabbath is part of God’s eternal moral law reminds me of the proof that 2 is an odd number. It goes like this:</p>
<ol class="arabic">
<li><p>Consider the <a class="reference external" href="https://en.wikipedia.org/wiki/Prime_number">prime numbers</a>. They are, by definition, positive integers that are divisible only by 1 and themselves. The sequence starts: 2, 3, 5, 7, 11, 13, 17, 19, …</p>
<p>As you’ll notice, they are all odd numbers — look at them: 3 is odd, 5 is odd, 7 is odd, 9 is odd. All the prime numbers are odd.</p>
<p>– “Excuse me, what about 2? That looks even to me…”</p>
<p>– We don’t talk about 2. (I’ll see you after class).</p>
<p>As I was saying, <strong>all</strong> the prime numbers are odd.</p>
</li>
<li><p>The number 2 is a prime number.</p></li>
<li><p>Therefore, 2 is an odd number. QED.</p></li>
</ol>
<p>The proof that the Sabbath is an eternal moral command looks the same:</p>
<ol class="arabic">
<li><p>All the Ten Commandments are God’s eternal moral law.</p>
<p>Look at them: “Do not murder” – a moral command that existed before Moses, and is repeated in the NT. And so it is with all of them – “You shall have no other Gods before me”, “Do not commit adultery” etc.</p>
<p>– “Excuse me, what about the 4th commandment? It seems pretty clear that the Sabbath was given specially to the Jews as a covenant sign, and the NT tells us that we don’t have holy days any more because they are fulfilled in Christ…”</p>
<p>– We don’t talk about the 4th commandment. And please don’t interrupt.</p>
<p>As you can see, <strong>all</strong> of the Ten Commandments are God’s eternal moral law.</p>
</li>
<li><p>The Sabbath law is part of the Ten Commandments</p></li>
<li><p>Therefore the Sabbath is an eternal moral command. And we celebrate it on Sundays, obviously.</p></li>
</ol>
<p>Even if we were to conclude the Sabbath is a moral command and must continue, we're not free to make up <strong>how</strong> it should continue. The NT actually gives us no ground for saying the Jewish Sabbath has been moved to the Lord’s Day. We would be left saying that it continues just as it is in the OT (producing many difficulties which I won't go into) — or, it continues and applies in the New Covenant age in the way described in Hebrews, that is, in a spiritual way as above (in other words, a long way round to the non-Sabbatarian position).</p>
<p>In fact, the NT is clear that the command is fulfilled in Christ just as other ceremonial commands are. We're not left in the dark about how to understand it. If we attempt to put observance of the Lord's Day as a Sabbath into a moral category, we produce an impossible situation when it comes treating people who fail to observe it. For matters of plain morality, we are required by scripture to judge people, to the extent of putting them out of the church and not even keeping company with them — “expel the wicked person” (<a class="reference external" href="https://www.biblegateway.com/passage/?search=1+Corinthians+5&version=NIVUK">1 Corinthians 5</a>). When it comes to observing holy days, <a class="reference external" href="https://www.biblegateway.com/passage/?search=Romans%2014&version=NIVUK">Romans 14</a> tells us that we must not judge each other, but rather accept one another (v1, 5, 13). To claim, as some do, that Romans 14 is talking about sacred days apart from the Lord's Day is simply special pleading, as there is no basis for saying so. This is a simple <em>reductio ad absurdum</em> that shows we erred when making literal Sabbath day observance a NT obligation. Rather than it being logically inescapable that the Lord's Day is to be observed as a Sabbath — which is the standard required for us to teach other people to so observe it — the reverse is closer to the truth.</p>
</section>
<section id="conclusion">
<h2>Conclusion</h2>
<p>To conclude the argument from Scripture:</p>
<p>The idea stated in the Westminster Confession that the Lord’s Day is to be the Sabbath from the resurrection of Christ to the end of the world cannot be found in Scripture, just as its statement about Sabbath observance “from the beginning of the world” is also insufficiently supported by the Bible. There are no statements whatsoever supporting the claim that the Sabbath must be observed on the Lord’s Day to this effect, either by precept, example or implication. If this idea comes from the Bible at all, it only does so by one possible extrapolation among several, and not by “good and necessary consequence”, which is the standard any teaching must pass before it can be taught from our pulpits. Further, it is an extrapolation that contradicts how the Bible itself handles the subject.</p>
<p><strong>However:</strong></p>
<p>The principle behind the need to set time aside to worship God can certainly apply to how we use Sunday (as well as other times in the week), especially if we have the freedom to use Sunday in a way that we choose. We also have the freedom as believers to “observe” the Lord’s day if we want to, whatever we mean by that — but not to put that requirement onto others (<a class="reference external" href="https://www.biblegateway.com/passage/?search=Romans%2014%3A5-6&version=NIVUK">Romans 14:5-6</a>). There is also the pattern that NT believers have handed on of meeting together on the Lord's Day, and the commandment in <a class="reference external" href="https://www.biblegateway.com/passage/?search=Hebrews%2010%3A25&version=NIVUK">Hebrews 10:25</a> to not forsake meeting together, which also mean that for most people, setting aside time to meet with God's people on Sunday must be a high priority.</p>
<p>For myself, with my work situation meaning that I have the freedom to rest on a Sunday (when I'm not preaching), I've found it an enormously helpful practice, and one that I commend to everyone. In fact, I would be suspicious of myself and my walk with God if I was preferring to do other things on the Lord’s Day — I've got the other days of the week when I can work. My practice has changed relatively little since I've come to a non-Sabbatarian position. But making this a binding rule on others, or even on myself, is not something that Scripture allows me to do.</p>
<p>There is also the principle of “rest”, which is big topic and it’s not my purpose to look at it in this post. While I couldn’t agree with every word of it, I found <a class="reference external" href="https://podcasts.apple.com/gb/podcast/timothy-keller-sermons-podcast-by-gospel-in-life/id352660924?i=1000601090144">Tim Keller’s sermon on Work and Rest</a> to be really helpful.</p>
</section>
<section id="appendix-early-church">
<h2>Appendix – Early church</h2>
<p>While it is Scripture and Scripture alone that settles the matter, the Early Church is also of interest. To diagnose our own blind spots it is often helpful to look to what the Church has historically believed. The earlier you go, the less likely it is, in general, that waters are muddied by traditions of men that have been added.</p>
<p>[UPDATE 2023-09-11] In addition, correct interpretation of some of the key texts mentioned above has often been overridden on the basis of historical claims that turn out not to be true. One example of this was furnished by a commenter below, <a class="reference external" href="https://purelypresbyterian.com/2018/10/15/is-the-sabbath-optional-an-evaluation-of-romans-14-and-colossians-2/">whose website</a> quotes from Wilhelmus à Brakel:</p>
<blockquote>
<p>Secondly, it is a well-known truth that the apostles commanded the churches everywhere to observe the Lord’s day (refer to the above). It is common knowledge that there was neither any contention concerning that day, nor was there any intent to force or eradicate the observance of this day contrary to the wishes of the apostles.</p>
</blockquote>
<p>I’ve heard this argument many times; it misled me in the past and continues to mislead people today. So I’m indebted to the person commenting below for providing a good example of it!</p>
<p>What is presented above as “well-known truth” is in fact false, or at best obscuring the truth. While the practice of meeting together on Sunday to celebrate the resurrection was indeed a widespread tradition that originated from earliest times, to call it “observing” the day, or claim the apostles commanded “observance” in the sense needed for a Sabbatarian view, is directly opposed to the evidence we have. For this argument to have any weight, its proponents need to show the actual Biblical or historical evidence demonstrating which things the apostles commanded to be done or not done on the Lord’s Day — hiding behind “it’s common knowledge” simply will not do. [end update].</p>
<p>I have not been able to find any evidence of Christian Sabbatarianism at all in the first two centuries. Many sources suggest some Christians continued to observe the Jewish Sabbath (i.e. Saturday) for centuries, but I haven't yet found an early source for that.</p>
<p>In general, the sources describe the practice of Christians meeting together on the Lord’s Day as being pretty much universal, but without making it a Sabbath day.</p>
<p>Origen in 220 AD is the first to say that the Lord’s Day should be observed as a day of rest, but he seems to be out of line with most people of his time, who made no such rules.</p>
<p>Very clear quotes on the subject from early Christians, including early believers like Justyn Martyr, and authorities like Tertullian and Augustine etc. can be found at <a class="reference external" href="http://www.bible.ca/H-sunday.htm">http://www.bible.ca/H-sunday.htm</a> and are copied below.</p>
<p>They are quite explicit about Christians not observing the Sabbath, and not being required to — and in fact you are overthrowing the gospel if you do (Chrysostom)! The word Sabbath is used exclusively of Jewish holy days, or in a strictly spiritual sense that doesn’t involve obeying any Sabbath-day regulations, but rather resting in the gospel and living in general holiness of life.</p>
<p>Where they talk about Christians “observing” the Lord’s Day (which mostly starts from about 3rd/4th century), it is as a contrast to observing the Sabbath, the main requirement being that Christians be joyful and that they meet together, and not that they refrain from any activity — which is called Jewish superstition and idleness.</p>
<p>Put together, they present overwhelming evidence that there is not a hint of a “Christian Sabbath” tradition (that fits with the Westminster Confession’s idea of what such as day is like) that was passed down from the apostles.</p>
<p>Justin Martyr is worth looking at in some detail:</p>
<section id="justin-martyr-dialogue-with-trypho-circa-130-150-ad">
<h3>Justin Martyr: Dialogue with Trypho (circa 130 - 150 AD)</h3>
<p>This is a report of a long debate with some Jews, in which the subject of Sabbath and circumcision comes up several times. It's extremely clear that Justin Martyr did not consider Christians to be bound to observe the Sabbath or sabbath days, and had an understanding of the Sabbath exactly in line with what I have written above, often with the same proof texts.</p>
<blockquote>
<p><a class="reference external" href="https://ccel.org/ccel/justin_martyr/dialog_with_trypho/anf01.viii.iv.x.html">Chapter X</a></p>
<p>And when they ceased, I again addressed them thus:—</p>
<p>“Is there any other matter, my friends, in which we are blamed, than this, that we live not after the law, and are not circumcised in the flesh as your forefathers were, and do not observe sabbaths as you do?</p>
<p>Trypho:</p>
<p>But this is what we are most at a loss about: that you, professing to be pious, and supposing yourselves better than others, are not in any particular separated from them, and do not alter your mode of living from the nations, in that you observe no festivals or sabbaths, and do not have the rite of circumcision; and further, resting your hopes on a man that was crucified, you yet expect to obtain some good thing from God, while you do not obey His commandments.</p>
<p><a class="reference external" href="https://ccel.org/ccel/justin_martyr/dialog_with_trypho/anf01.viii.iv.xii.html">Chapter XII</a></p>
<p>Justin Martyr:</p>
<p>I also adduced another passage in which Isaiah exclaims: “ ‘Hear My words, and your soul shall live; and I will make an everlasting covenant with you, even the sure mercies of David. Behold, I have given Him for a witness to the people: nations which know not Thee shall call on Thee; peoples who know not Thee shall escape to Thee, because of thy God, the Holy One of Israel; for He has glorified Thee.’ This same law you have despised, and His new holy covenant you have slighted; and now you neither receive it, nor repent of your evil deeds. ‘For your ears are closed, your eyes are blinded, and the heart is hardened,’ Jeremiah has cried; yet not even then do you listen. The Lawgiver is present, yet you do not see Him; to the poor the Gospel is preached, the blind see, yet you do not understand. You have now need of a second circumcision, though you glory greatly in the flesh. The new law requires you to keep perpetual sabbath, and you, because you are idle for one day, suppose you are pious, not discerning why this has been commanded you: and if you eat unleavened bread, you say the will of God has been fulfilled. The Lord our God does not take pleasure in such observances: if there is any perjured person or a thief among you, let him cease to be so; if any adulterer, let him repent; then he has kept the sweet and true sabbaths of God. If any one has impure hands, let him wash and be pure.</p>
<p><a class="reference external" href="https://ccel.org/ccel/justin_martyr/dialog_with_trypho/anf01.viii.iv.xviii.html">Chapter XVIII</a></p>
<p>“For since you have read, O Trypho, as you yourself admitted, the doctrines taught by our Saviour, I do not think that I have done foolishly in adding some short utterances of His to the prophetic statements. Wash therefore, and be now clean, and put away iniquity from your souls, as God bids you be washed in this laver, and be circumcised with the true circumcision. For we too would observe the fleshly circumcision, and the Sabbaths, and in short all the feasts, if we did not know for what reason they were enjoined you,—namely, on account of your transgressions and the hardness of your hearts. For if we patiently endure all things contrived against us by wicked men and demons, so that even amid cruelties unutterable, death and torments, we pray for mercy to those who inflict such things upon us, and do not wish to give the least retort to any one, even as the new Lawgiver commanded us: how is it, Trypho, that we would not observe those rites which do not harm us, —I speak of fleshly circumcision, and Sabbaths, and feasts?</p>
<p><a class="reference external" href="https://ccel.org/ccel/justin_martyr/dialog_with_trypho/anf01.viii.iv.xix.html">Chapter XIX</a></p>
<p>Therefore to you alone this circumcision was necessary, in order that the people may be no people, and the nation no nation; as also Hosea, one of the twelve prophets, declares. Moreover, all those righteous men already mentioned [Abel, Enoch, Noah, Melchizedek], though they kept no Sabbaths, were pleasing to God; and after them Abraham with all his descendants until Moses, under whom your nation appeared unrighteous and ungrateful to God, making a calf in the wilderness: wherefore God, accommodating Himself to that nation, enjoined them also to offer sacrifices, as if to His name, in order that you might not serve idols. Which precept, however, you have not observed; nay, you sacrificed your children to demons. And you were commanded to keep Sabbaths, that you might retain the memorial of God. For His word makes this announcement, saying, ‘That ye may know that I am God who redeemed you.’</p>
<p><a class="reference external" href="https://ccel.org/ccel/justin_martyr/dialog_with_trypho/anf01.viii.iv.xxi.html">Chapter XXI</a></p>
<p>“Moreover, that God enjoined you to keep the Sabbath, and impose on you other precepts for a sign, as I have already said, on account of your unrighteousness, and that of your fathers,—as He declares that for the sake of the nations, lest His name be profaned among them, therefore He permitted some of you to remain alive,—these words of His can prove to you: they are narrated by Ezekiel thus: ‘I am the Lord your God; walk in My statutes, and keep My judgements, and take no part in the customs of Egypt; and hallow My Sabbaths; and they shall be a sign between Me and you, that ye may know that I am the Lord your God. Notwithstanding ye rebelled against Me, and your children walked not in My statutes, neither kept My judgements to do them: which if a man do, he shall live in them. But they polluted My Sabbaths. And I said that I would pour out My fury upon them in the wilderness, to accomplish My anger upon them; yet I did it not; that My name might not be altogether profaned in the sight of the heathen. I led them out before their eyes, and I lifted up Mine hand unto them in the wilderness, that I would scatter them among the heathen, and disperse them through the countries; because they had not executed My judgements, but had despised My statutes, and polluted My Sabbaths, and their eyes were after the devices of their fathers. Wherefore I gave them also statutes which were not good, and judgements whereby they shall not live. And I shall pollute them in their own gifts, that I may destroy all that openeth the womb, when I pass through them.’</p>
</blockquote>
</section>
<section id="mathetes-epistle-to-diognetus">
<h3>Mathetes: Epistle to Diognetus</h3>
<p>I also came across this work, dating from AD 130 to the end of the century, which is relevant for its general tenor:</p>
<blockquote>
<p><a class="reference external" href="https://ccel.org/ccel/mathetes/epistle_of_mathetes_to_diognetus/anf01.iii.ii.iv.html">Chapter IV.—The other observances of the Jews.</a></p>
<p>But as to their scrupulosity concerning meats, and their superstition as respects the Sabbaths, and their boasting about circumcision, and their fancies about fasting and the new moons, which are utterly ridiculous and unworthy of notice,—I do not think that you require to learn anything from me.</p>
<p><a class="reference external" href="https://ccel.org/ccel/mathetes/epistle_of_mathetes_to_diognetus/anf01.iii.ii.v.html">Chapter V.—The manners of the Christians.</a></p>
<p>For the Christians are distinguished from other men neither by country, nor language, nor the customs which they observe. For they neither inhabit cities of their own, nor employ a peculiar form of speech, nor lead a life which is marked out by any singularity. The course of conduct which they follow has not been devised by any speculation or deliberation of inquisitive men; nor do they, like some, proclaim themselves the advocates of any merely human doctrines. But, inhabiting Greek as well as barbarian cities, according as the lot of each of them has determined, and following the customs of the natives in respect to clothing, food, and the rest of their ordinary conduct, they display to us their wonderful and confessedly striking method of life. They dwell in their own countries, but simply as sojourners. As citizens, they share in all things with others, and yet endure all things as if foreigners. Every foreign land is to them as their native country, and every land of their birth as a land of strangers. They marry, as do all [others]; they beget children; but they do not destroy their offspring. They have a common table, but not a common bed. They are in the flesh, but they do not live after the flesh. They pass their days on earth, but they are citizens of heaven. They obey the prescribed laws, and at the same time surpass the laws by their lives. They love all men, and are persecuted by all. They are unknown and condemned; they are put to death, and restored to life. They are poor, yet make many rich; they are in lack of all things, and yet abound in all; they are dishonoured, and yet in their very dishonour are glorified. They are evil spoken of, and yet are justified; they are reviled, and bless; they are insulted, and repay the insult with honour; they do good, yet are punished as evil-doers.</p>
</blockquote>
</section>
<section id="other-early-church-and-church-fathers-writings">
<h3>Other Early Church and Church Fathers writings</h3>
<p>The following are taken verbatim (including comments) from <a class="reference external" href="http://www.bible.ca/H-sunday.htm">http://www.bible.ca/H-sunday.htm</a> . I have checked the accuracy of some, but not most of them.</p>
<ul class="simple">
<li><p>90AD DIDACHE: "Christian Assembly on the Lord’s Day: 1. But every Lord’s day do ye gather yourselves together, and break bread, and give thanksgiving after having confessed your transgressions, that your sacrifice may be pure. 2. But let no one that is at variance with his fellow come together with you, until they be reconciled, that your sacrifice may not be profaned. 3. For this is that which was spoken by the Lord: In every place and time offer to me a pure sacrifice; for I am a great King, saith the Lord, and my name is wonderful among the nations." (Didache: The Teaching of the Twelve Apostles, Chapter XIV)</p></li>
<li><p>100 AD BARNABAS "We keep the eighth day [Sunday] with joyfulness, the day also on which Jesus rose again from the dead" (The Epistle of Barnabas, 100 AD 15:6-8).</p></li>
<li><p>100 AD BARNABAS: Moreover God says to the Jews, 'Your new moons and Sabbaths 1 cannot endure.' You see how he says, 'The present Sabbaths are not acceptable to me, but the Sabbath which I have made in which, when I have rested [heaven: Heb 4] from all things, I will make the beginning of the eighth day which is the beginning of another world.' Wherefore we Christians keep the eighth day for joy, on which also Jesus arose from the dead and when he appeared ascended into heaven. (15:8f, The Epistle of Barnabas, 100 AD, Ante-Nicene Fathers , vol. 1, pg. 147)</p></li>
<li><p>110AD Pliny: "they were in the habit of meeting on a certain fixed day before it was light, when they sang in alternate verses a hymn to Christ, as to a god, and bound themselves by a solemn oath not to (do) any wicked deeds, never to commit any fraud, theft, or adultery, never to falsify their word, nor deny a trust when they should be called upon to deliver it up; after which it was their custom to separate, and then reassemble to partake of good food—but food of an ordinary and innocent kind."</p></li>
<li><p>150AD EPISTLE OF THE APOSTLES.- I [Christ] have come into being on the eighth day which is the day of the Lord. (18)</p></li>
<li><p>150AD JUSTIN: "He then speaks of those Gentiles, namely us, who in every place offer sacrifices to Him, i.e., the bread of the Eucharist, and also the cup of the Eucharist, affirming both that we glorify His name, and that you profane [it]. The command of circumcision, again, bidding [them] always circumcise the children on the eighth day, was a type of the true circumcision, by which we are circumcised from deceit and iniquity through Him who rose from the dead on the first day after the Sabbath, [namely through] our Lord Jesus Christ. For the first day after the Sabbath, remaining the first of all the days, is called, however, the eighth, according to the number of all the days of the cycle, and [yet] remains the first.". (Justin, Dialogue 41:4)</p></li>
<li><p>150AD JUSTIN: …those who have persecuted and do persecute Christ, if they do not repent, shall not inherit anything on the holy mountain. But the Gentiles, who have believed on Him, and have repented of the sins which they have committed, they shall receive the inheritance along with the patriarchs and the prophets, and the just men who are descended from Jacob, even although they neither keep the Sabbath, nor are circumcised, nor observe the feasts. Assuredly they shall receive the holy inheritance of God. (Dialogue With Trypho the Jew, 150-165 AD, Ante-Nicene Fathers, vol. 1, page 207)</p></li>
<li><p>150AD JUSTIN: But if we do not admit this, we shall be liable to fall into foolish opinion, as if it were not the same God who existed in the times of Enoch and all the rest, who neither were circumcised after the flesh, nor observed Sabbaths, nor any other rites, seeing that Moses enjoined such observances… For if there was no need of circumcision before Abraham, or of the observance of Sabbaths, of feasts and sacrifices, before Moses; no more need is there of them now, after that, according to the will of God, Jesus Christ the Son of God has been born without sin, of a virgin sprung from the stock of Abraham. (Dialogue With Trypho the Jew, 150-165 AD, Ante-Nicene Fathers , vol. 1, page 206)</p></li>
<li><p>150AD JUSTIN: "And on the day called Sunday, all who live in cities or in the country gather together to one place, and the memoirs of the apostles or the writings of the prophets are read, as long as time permits; then, when the reader has ceased, the president verbally instructs, and exhorts to the imitation of these good things. Then we all rise together and pray, and, as we before said, when our prayer is ended, bread and wine and water are brought, and the president in like manner offers prayers and thanksgivings, according to his ability, and the people assent, saying Amen; and there is a distribution to each, and a participation of that over which thanks have been given, and to those who are absent a portion is sent by the deacons. And they who are well to do, and willing, give what each thinks fit; and what is collected is deposited with the president, who succours the orphans and widows and those who, through sickness or any other cause, are in want, and those who are in bonds and the strangers sojourning among us, and in a word takes care of all who are in need. But Sunday is the day on which we all hold our common assembly, because it is the first day on which God, having wrought a change in the darkness and matter, made the world; and Jesus Christ our Saviour on the same day rose from the dead. For He was crucified on the day before that of Saturn (Saturday); and on the day after that of Saturn, which is the day of the Sun, having appeared to His apostles and disciples, He taught them these things, which we have submitted to you also for your consideration." (First apology of Justin, Weekly Worship of the Christians, Ch 68)</p></li>
<li><p>150AD JUSTIN: Moreover, all those righteous men already mentioned [after mentioning Adam. Abel, Enoch, Lot, Noah, Melchizedek, and Abraham], though they kept no Sabbaths, were pleasing to God; and after them Abraham with all his descendants until Moses… And you [fleshly Jews] were commanded to keep Sabbaths, that you might retain the memorial of God. For His word makes this announcement, saying, "That you may know that I am God who redeemed you." (Dialogue With Trypho the Jew, 150-165 AD, Ante-Nicene Fathers , vol. 1, page 204)</p></li>
<li><p>150AD JUSTIN: There is no other thing for which you blame us, my friends, is there than this? That we do not live according to the Law, nor, are we circumcised in the flesh as your forefathers, nor do we observe the Sabbath as you do. (Dialogue with Trypho 10:1. In verse 3 the Jew Trypho acknowledges that Christians 'do not keep the Sabbath.')</p></li>
<li><p>150AD JUSTIN: We are always together with one another. And for all the things with which we are supplied we bless the Maker of all through his Son Jesus Christ and through his Holy Spirit. And on the day called Sunday there is a gathering together in the same place of all who live in a city or a rural district. [There follows an account of a Christian worship service, which is quoted in VII.2.] We all make our assembly in common on the day of the Sun, since it is the first day, on which God changed the darkness and matter and made the world, and Jesus Christ our Savior arose from the dead on the same day. For they crucified him on the day before Saturn's day, and on the day after (which is the day of the Sun the appeared to his apostles and taught his disciples these things. (Apology, 1, 67:1-3, 7; First Apology, 145 AD, Ante-Nicene Fathers , Vol. 1, pg. 186)</p></li>
<li><p>155 AD Justin Martyr "[W]e too would observe the fleshly circumcision, and the Sabbaths, and in short all the feasts, if we did not know for what reason they were enjoined [on] you–namely, on account of your transgressions and the hardness of your heart. . . . [H]ow is it, Trypho, that we would not observe those rites which do not harm us–I speak of fleshly circumcision and Sabbaths and feasts? . . . God enjoined you [Jews] to keep the Sabbath, and impose on you other precepts for a sign, as I have already said, on account of your unrighteousness and that of your fathers" (Dialogue with Trypho the Jew 18, 21).</p></li>
<li><p>180AD ACTS OF PETER.- Paul had often contended with the Jewish teachers and had confuted them, saying 'it is Christ on whom your fathers laid hands. He abolished their Sabbath and fasts and festivals and circumcision.' (1: I)-2</p></li>
<li><p>190AD CLEMENT OF ALEXANDRIA: (in commenting on each of the Ten Commandments and their Christian meaning:) The seventh day is proclaimed a day of rest, preparing by abstention from evil for the Primal day, our true rest. (Ibid. VII. xvi. 138.1)</p></li>
<li><p>190AD CLEMENT OF ALEXANDRIA: He does the commandment according to the Gospel and keeps the Lord’s day, whenever he puts away an evil mind . . . glorifying the Lord’s resurrection in himself. (Ibid. Vii.xii.76.4)</p></li>
<li><p>190AD CLEMENT OF ALEXANDRIA: Plato prophetically speaks of the Lord’s day in the tenth book of the Republic, in these words: 'And when seven days have passed to each of them in the meadow, on the eighth they must go on." (Miscellanies V.xiv.106.2)</p></li>
<li><p>200AD BARDESANES: Wherever we are, we are all called after the one name of Christ Christians. On one day, the first of the week, we assemble ourselves together (On Fate)</p></li>
<li><p>200AD TERTULLIAN: "We solemnize the day after Saturday in contradistinction to those who call this day their Sabbath" (Tertullian's Apology, Ch 16)</p></li>
<li><p>200AD TERTULLIAN: It follows, accordingly, that, in so far as the abolition of carnal circumcision and of the old law is demonstrated as having been consummated at its specific times, so also the observance of the Sabbath is demonstrated to have been temporary. (An Answer to the Jews 4:1, Ante-Nicene Fathers Vol. 3, page 155)</p></li>
<li><p>200AD TERTULLIAN: Let him who contends that the Sabbath is still to be observed a balm of salvation, and circumcision on the eighth day because of threat of death, teach us that in earliest times righteous men kept Sabbath or practiced circumcision, and so were made friends of God. .. …Therefore, since God originated Adam uncircumcised, and inobservant of the Sabbath, consequently his offspring also, Abel, offering Him sacrifices, uncircumcised and inobservant of the Sabbath, was by Him commended… Noah also, uncircumcised - yes, and inobservant of the Sabbath - God freed from the deluge. For Enoch, too, most righteous man, uncircumcised and inobservant of the Sabbath, He translated from this world… Melchizedek also, "the priest of most high God," uncircumcised and inobservant of the Sabbath, was chosen to the priesthood of God. (An Answer to the Jews 2:10; 4:1, Ante-Nicene Fathers Vol. 3, page 153)</p></li>
<li><p>200AD TERTULLIAN: Others . . . suppose that the sun is the god of the Christians, because it is well-known that we regard Sunday as a day of joy. (To the Nations 1: 133)</p></li>
<li><p>200AD TERTULLIAN: To us Sabbaths are foreign. (On Idolatry, 14:6)</p></li>
<li><p>220AD ORIGEN "On Sunday none of the actions of the world should be done. If then, you abstain from all the works of this world and keep yourselves free for spiritual things, go to church, listen to the readings and divine homilies, meditate on heavenly things. (Homil. 23 in Numeros 4, PG 12:749)</p></li>
<li><p>220 AD Origen "Hence it is not possible that the [day of] rest after the Sabbath should have come into existence from the seventh [day] of our God. On the contrary, it is our Savior who, after the pattern of his own rest, caused us to be made in the likeness of his death, and hence also of his resurrection" (Commentary on John 2:28).</p></li>
<li><p>225 AD The Didascalia "The apostles further appointed: On the first day of the week let there be service, and the reading of the Holy Scriptures, and the oblation, because on the first day of the week our Lord rose from the place of the dead, and on the first day of the week he arose upon the world, and on the first day of the week he ascended up to heaven, and on the first day of the week he will appear at last with the angels of heaven" (Didascalia 2).</p></li>
<li><p>250AD CYPRIAN: The eight day, that is, the first day after the Sabbath, and the Lord’s Day." (Epistle 58, Sec 4)</p></li>
<li><p>250 AD IGNATIUS: "If, therefore, those who were brought up in the ancient order of things have come to the possession of a new hope, no longer observing the Sabbath, but living in the observance of the Lord’s Day, on which also our life has sprung up again by Him and by His death-whom some deny, by which mystery we have obtained faith, and therefore endure, that we may be found the disciples of Jesus Christ, our only Master-how shall we be able to live apart from Him, whose disciples the prophets themselves in the Spirit did wait for Him as their Teacher? And therefore He whom they rightly waited for, being come, raised them from the dead. If, then, those who were conversant with the ancient Scriptures came to newness of hope, expecting the coming of Christ, as the Lord teaches us when He says, "If ye had believed Moses, ye would have believed Me, for he wrote of Me; " and again, "Your father Abraham rejoiced to see My day, and he saw it, and was glad; for before Abraham was, I am; " how shall we be able to live without Him? The prophets were His servants, and foresaw Him by the Spirit, and waited for Him as their Teacher, and expected Him as their Lord and Saviour, saying, "He will come and save us." Let us therefore no longer keep the Sabbath after the Jewish manner, and rejoice in days of idleness; for "he that does not work, let him not eat." For say the [holy] oracles, "In the sweat of thy face shalt thou eat thy bread." But let every one of you keep the Sabbath after a spiritual manner, rejoicing in meditation on the law, not in relaxation of the body, admiring the workmanship of God, and not eating things prepared the day before, nor using lukewarm drinks, and walking within a prescribed space, nor finding delight in dancing and plaudits which have no sense in them. And after the observance of the Sabbath, let every friend of Christ keep the Lord’s Day as a festival, the resurrection-day, the queen and chief of all the days [of the week]. Looking forward to this, the prophet declared, "To the end, for the eighth day," on which our life both sprang up again, and the victory over death was obtained in Christ, whom the children of perdition, the enemies of the Saviour, deny, "whose god is their belly, who mind earthly things," who are "lovers of pleasure, and not lovers of God, having a form of godliness, but denying the power thereof." These make merchandise of Christ, corrupting His word, and giving up Jesus to sale: they are corrupters of women, and covetous of other men's possessions, swallowing up wealth insatiably; from whom may ye be delivered by the mercy of God through our Lord Jesus Christ! (Epistle of Ignatius to the Magnesians, Chapter IX)</p></li>
<li><p>250AD IGNATIUS: "On the day of the preparation, then, at the third hour, He received the sentence from Pilate, the Father permitting that to happen; at the sixth hour He was crucified; at the ninth hour He gave up the ghost; and before sunset He was buried. During the Sabbath He continued under the earth in the tomb in which Joseph of Arimathaea had laid Him. At the dawning of the Lord’s day He arose from the dead, according to what was spoken by Himself, "As Jonah was three days and three nights in the whale's belly, so shall the Son of man also be three days and three nights in the heart of the earth." The day of the preparation, then, comprises the passion; the Sabbath embraces the burial; the Lord’s Day contains the resurrection." (The Epistle of Ignatius to the Trallians, chapter 9)</p></li>
<li><p>250AD IGNATIUS: If any one fasts on the Lord’s Day or on the Sabbath, except on the paschal Sabbath only, he is a murderer of Christ. (The Epistle of Ignatius to the Philippians, chapter 8)</p></li>
<li><p>250AD IGNATIUS: "This [custom], of not bending the knee upon Sunday, is a symbol of the resurrection, through which we have been set free, by the grace of Christ, from sins, and from death, which has been put to death under Him. Now this custom took its rise from apostolic times, as the blessed Irenaeus, the martyr and bishop of Lyons, declares in his treatise On Easter, in which he makes mention of Pentecost also; upon which [feast] we do not bend the knee, because it is of equal significance with the Lord’s day, for the reason already alleged concerning it." (Ignatius, Fragments)</p></li>
<li><p>300 AD Victorinus "The sixth day [Friday] is called parasceve, that is to say, the preparation of the kingdom. . . . On this day also, on account of the passion of the Lord Jesus Christ, we make either a station to God or a fast. On the seventh day he rested from all his works, and blessed it, and sanctified it. On the former day we are accustomed to fast rigorously, that on the Lord’s day we may go forth to our bread with giving of thanks. And let the parasceve become a rigorous fast, lest we should appear to observe any Sabbath with the Jews . . . which Sabbath he [Christ] in his body abolished" (The Creation of the World).</p></li>
<li><p>300AD EUSEBIUS: "They did not, therefore, regard circumcision, nor observe the Sabbath neither do we; … because such things as these do not belong to Christians" (Ecc. Hist., Book 1, Ch. 4)</p></li>
<li><p>300AD EUSEBIUS: [The Ebionites] were accustomed to observe the Sabbath and other Jewish customs but on the Lord’s days to celebrate the same practices as we in remembrance of the resurrection of the Savior. (Church History Ill.xxvii.5)</p></li>
<li><p>300 AD Eusebius of Caesarea "They [the pre- Mosaic saints of the Old Testament] did not care about circumcision of the body, neither do we [Christians]. They did not care about observing Sabbaths, nor do we. They did not avoid certain kinds of food, neither did they regard the other distinctions which Moses first delivered to their posterity to be observed as symbols; nor do Christians of the present day do such things" (Church History 1:4:8).</p></li>
<li><p>300 AD Eusebius of Caesarea "The day of his [Christ's] light . . . was the day of his resurrection from the dead, which they say, as being the one and only truly holy day and the Lord’s day, is better than any number of days as we ordinarily understand them, and better than the days set apart by the Mosaic Law for feasts, new moons, and Sabbaths, which the Apostle [Paul] teaches are the shadow of days and not days in reality" (Proof of the Gospel 4:16:186).</p></li>
<li><p>345 AD Athanasius "The Sabbath was the end of the first creation, the Lord’s day was the beginning of the second, in which he renewed and restored the old in the same way as he prescribed that they should formerly observe the Sabbath as a memorial of the end of the first things, so we honor the Lord’s day as being the memorial of the new creation" (On Sabbath and Circumcision 3).</p></li>
<li><p>350 AD APOSTOLIC CONSTITUTIONS: Be not careless of yourselves, neither deprive your Saviour of His own members, neither divide His body nor disperse His members, neither prefer the occasions of this life to the word of God; but assemble yourselves together every day, morning and evening, singing psalms and praying in the Lord’s house: in the morning saying the sixty-second Psalm, and in the evening the hundred and fortieth, but principally on the Sabbath-day. And on the day of our Lord’s resurrection, which is the Lord’s day, meet more diligently, sending praise to God that made the universe by Jesus, and sent Him to us, and condescended to let Him suffer, and raised Him from the dead. Otherwise what apology will he make to God who does not assemble on that day to hear the saving word concerning the resurrection, on which we pray thrice standing in memory of Him who arose in three days, in which is performed the reading of the prophets, the preaching of the Gospel, the oblation of the sacrifice, the gift of the holy food? (Constitutions of the Holy Apostles, book 2)</p></li>
<li><p>350 AD APOSTOLIC CONSTITUTIONS: For if the Gentiles every day, when they arise from sleep, run to their idols to worship them, and before all their work and all their labors do first of all pray to them, and in their feasts and in their solemnities do not keep away, but attend upon them; and not only those upon the place, but those living far distant do the same; and in their public shows all come together, as into a synagogue: in the same manner those which are vainly called Jews, when they have worked six days, on the seventh day rest, and come together in their synagogue, never leaving or neglecting either rest from labor or assembling together… If, therefore, those who are not saved frequently assemble together for such purposes as do not profit them, what apology wilt thou make to the Lord God who forsakes his Church, not imitating so much as the heathen, but by such, thy absence grows slothful, or turns apostate. or acts wickedness? To whom the Lord says to Jeremiah, "Ye have not kept My ordinances; nay, you have not walked according to the ordinance of the heathen and you have in a manner exceeded them… How, therefore, will any one make his apology who has despised or absented himself from the church of God? (Constitutions of the Holy Apostles, book 2)</p></li>
<li><p>350 AD APOSTOLIC CONSTITUTIONS: Do you therefore fast, and ask your petitions of God. We enjoin you to fast every fourth day of the week, and every day of the preparation, and the surplusage of your fast bestow upon the needy; every Sabbath-day excepting one, and every Lord’s day, hold your solemn assemblies, and rejoice: for he will be guilty of sin who fasts on the Lord’s day, being the day of the resurrection, or during the time of Pentecost, or, in general, who is sad on a festival day to the Lord For on them we ought to rejoice, and not to mourn. (Constitutions of the Holy Apostles, book 5)</p></li>
<li><p>350 AD APOSTOLIC CONSTITUTIONS "Which Days of the Week We are to Fast, and Which Not, and for What Reasons: But let not your fasts be with the hypocrites; for they fast on the second and fifth days of the week. But do you either fast the entire five days, or on the fourth day of the week, and on the day of the Preparation, because on the fourth day the condemnation went out against the Lord, Judas then promising to betray Him for money; and you must fast on the day of the Preparation, because on that day the Lord suffered the death of the cross under Pontius Pilate. But keep the Sabbath, and the Lord’s day festival; because the former is the memorial of the creation, and the latter of the resurrection. But there is one only Sabbath to be observed by you in the whole year, which is that of our Lord’s burial, on which men ought to keep a fast, but not a festival. For inasmuch as the Creator was then under the earth, the sorrow for Him is more forcible than the joy for the creation; for the Creator is more honourable by nature and dignity than His own creatures." (Constitutions of the Holy Apostles, book 7)</p></li>
<li><p>350 AD APOSTOLIC CONSTITUTIONS "How We Ought to Assemble Together, and to Celebrate the Festival Day of Our Saviour's Resurrection. On the day of the resurrection of the Lord, that is, the Lord’s day, assemble yourselves together, without fail, giving thanks to God, and praising Him for those mercies God has bestowed upon you through Christ, and has delivered you from ignorance, error, and bondage, that your sacrifice may be unspotted, and acceptable to God, who has said concerning His universal Church: "In every place shall incense and a pure sacrifice be offered unto me; for I am a great King, saith the Lord Almighty, and my name is wonderful among the heathen." (Constitutions of the Holy Apostles, book 7)</p></li>
<li><p>350 AD Cyril of Jerusalem "Fall not away either into the sect of the Samaritans or into Judaism, for Jesus Christ has henceforth ransomed you. Stand aloof from all observance of Sabbaths and from calling any indifferent meats common or unclean" (Catechetical Lectures 4:37).</p></li>
<li><p>360 AD Council of Laodicea "Christians should not Judaize and should not be idle on the Sabbath, but should work on that day; they should, however, particularly reverence the Lord’s day and, if possible, not work on it, because they were Christians" (canon 29).</p></li>
<li><p>387 AD John Chrysostom "You have put on Christ, you have become a member of the Lord and been enrolled in the heavenly city, and you still grovel in the Law [of Moses]? How is it possible for you to obtain the kingdom? Listen to Paul's words, that the observance of the Law overthrows the gospel, and learn, if you will, how this comes to pass, and tremble, and shun this pitfall. Why do you keep the Sabbath and fast with the Jews?" (Homilies on Galatians 2:17).</p></li>
<li><p>387 AD John Chrysostom "The rite of circumcision was venerable in the Jews' account, forasmuch as the Law itself gave way thereto, and the Sabbath was less esteemed than circumcision. For that circumcision might be performed, the Sabbath was broken; but that the Sabbath might be kept, circumcision was never broken; and mark, I pray, the dispensation of God. This is found to be even more solemn that the Sabbath, as not being omitted at certain times. When then it is done away, much more is the Sabbath" (Homilies on Philippians 10).</p></li>
<li><p>412 AD Augustine "Well, now, I should like to be told what there is in these Ten Commandments, except the observance of the Sabbath, which ought not to be kept by a Christian . . . Which of these commandments would anyone say that the Christian ought not to keep? It is possible to contend that it is not the Law which was written on those two tables that the apostle [Paul] describes as 'the letter that kills' [2 Cor. 3:6], but the law of circumcision and the other sacred rites which are now abolished" (The Spirit and the Letter 24).</p></li>
<li><p>597 AD Gregory I "It has come to my ears that certain men of perverse spirit have sown among you some things that are wrong and opposed to the holy faith, so as to forbid any work being done on the Sabbath day. What else can I call these [men] but preachers of Antichrist, who when he comes will cause the Sabbath day as well as the Lord’s day to be kept free from all work. For because he [the Antichrist] pretends to die and rise again, he wishes the Lord’s day to be had in reverence; and because he compels the people to Judaize that he may bring back the outward rite of the Law, and subject the perfidy of the Jews to himself, he wishes the Sabbath to be observed. For this which is said by the prophet, 'You shall bring in no burden through your gates on the Sabbath day' (Jer. 17:24) could be held to as long as it was lawful for the Law to be observed according to the letter. But after that the grace of almighty God, our Lord Jesus Christ, has appeared, the commandments of the Law which were spoken figuratively cannot be kept according to the letter. For if anyone says that this about the Sabbath is to be kept, he must needs say that carnal sacrifices are to be offered. He must say too that the commandment about the circumcision of the body is still to be retained. But let him hear the apostle Paul saying in opposition to him: 'If you be circumcised, Christ will profit you nothing' (Gal. 5:2)" (Letters 13:1).</p></li>
</ul>
</section>
</section>No one actually wants simplicityhttps://lukeplant.me.uk/blog/posts/no-one-actually-wants-simplicity/2023-08-22T18:49:31+01:002023-08-22T18:49:31+01:00Luke Plant<p>We think we do, but in fact every web developer will happily sacrifice simplicity to the first shiny thing promising them relief from the mildest of ailments.</p><p>The reason that modern web development is <a class="reference external" href="https://www.youtube.com/watch?v=BtJAsvJOlhM">swamped with complexity</a> is that no one really wants things to be simple. We just think we do, while our choices prove otherwise.</p>
<p>A lot of developers want simplicity in the same way that a lot of clients claim they want a fast website. You respond “OK, so we can remove some of these 17 Javascript trackers and other bloat that’s making your website horribly slow?” – no, apparently those are all critical business functionality.</p>
<p>In other words, they prioritise everything over speed. And then they wonder why using their website is like rowing a boat through a lake of molasses on a cold day using nothing but a small plastic spoon.</p>
<p>The same is often true of complexity. The real test is the question “what are you willing to sacrifice to achieve simplicity?” If the answer is “nothing”, then you don’t actually love simplicity at all, it’s your lowest priority.</p>
<p>When I say “sacrifice”, I don’t mean that choosing simplicity will mean you are worse off overall – simplicity brings massive benefits. But it does mean that there will be some things that tempt you to believe you are missing out.</p>
<p>For every developer, it might be something different. For one, the tedium of having to spend half an hour a month ensuring that two different things are kept in sync easily justifies the adoption of a bulky framework that solves that particular problem. For another, the ability to control how a checkbox animates when you check it is of course a valid reason to add another 50 packages and 3 layers of frameworks to their product. For another, adding an abstraction with thousands of lines of codes, dozens of classes and page after page of documentation in order to avoid manually writing a <a class="reference external" href="https://lukeplant.me.uk/blog/posts/test-factory-functions-in-django/">tiny factory function for a test</a> is a great trade-off.</p>
<p>Of course we all claim to hate complexity, but it’s actually just complexity added by other people that we hate — our own bugbears are always exempted, and for things we understand we quickly become unable to even see there is a potential problem for other people. Certainly there are frameworks and dependencies that justify their existence and adoption, but working out which ones they are is hard.</p>
<p>I think a good test of whether you truly love simplicity is whether you are able to remove things you have added, especially code you’ve written, even when it is still providing value, because you realise it is not providing enough value.</p>
<p>Another test is what you are tempted to do when a problem arises with some of the complexity you’ve added. Is your first instinct to add even more stuff to fix it, or is it to remove and live with the loss?</p>
<p>The only path I can see through all this is to cultivate an almost obsessive suspicion of <a class="reference external" href="https://en.wikipedia.org/wiki/Fear_of_missing_out">FOMO</a>. I think that’s probably key to learning to <a class="reference external" href="https://grugbrain.dev/#grug-on-saying-no">say no</a>.</p>
<section id="links">
<h2>Links</h2>
<ul class="simple">
<li><p><a class="reference external" href="https://lobste.rs/s/ao2x0v/no_one_actually_wants_simplicity">Discussion of this post on Lobsters</a></p></li>
</ul>
</section>You can stop using user-scalable=no and maximum-scale=1 in viewport meta tags nowhttps://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/2023-06-10T15:18:08+01:002023-06-10T15:18:08+01:00Luke Plant<p>It’s bad for accessibility, and no longer needed.</p><p>Many websites are still using a <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag">viewport meta tag</a> like one of the following:</p>
<div class="code"><pre class="code html"><a id="rest_code_4d92cabb2a4d467d8344234ba1fddc53-1" name="rest_code_4d92cabb2a4d467d8344234ba1fddc53-1" href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/#rest_code_4d92cabb2a4d467d8344234ba1fddc53-1"></a><span class="p"><</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">"viewport"</span> <span class="na">content</span><span class="o">=</span><span class="s">"width=device-width, initial-scale=1, maximum-scale=1"</span><span class="p">></span>
</pre></div>
<div class="code"><pre class="code html"><a id="rest_code_a07162cc66c24764bd33524416c4bc41-1" name="rest_code_a07162cc66c24764bd33524416c4bc41-1" href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/#rest_code_a07162cc66c24764bd33524416c4bc41-1"></a><span class="p"><</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">"viewport"</span> <span class="na">content</span><span class="o">=</span><span class="s">"width=device-width, initial-scale=1, user-scalable=no"</span><span class="p">></span>
</pre></div>
<p>These days, you can almost certainly remove the <code class="docutils literal"><span class="pre">maximum-scale</span></code> or <code class="docutils literal"><span class="pre">user-scalable</span></code> properties, to leave:</p>
<div class="code"><pre class="code html"><a id="rest_code_70e17207c26d489f8688406a10ea8e3d-1" name="rest_code_70e17207c26d489f8688406a10ea8e3d-1" href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/#rest_code_70e17207c26d489f8688406a10ea8e3d-1"></a><span class="p"><</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">"viewport"</span> <span class="na">content</span><span class="o">=</span><span class="s">"width=device-width, initial-scale=1"</span><span class="p">></span>
</pre></div>
<p>This is the same as suggested by <a class="reference external" href="https://github.com/h5bp/html5-boilerplate/blob/main/src/index.html">HTML5 boilerplate</a>, so it should be a pretty good default for most people.</p>
<p>Why should you remove these properties? Because they’re bad for accessibility — they stop users on many mobile devices (mostly Android) from being able to zoom in and view things that would be too small otherwise. This doesn’t just affect people with impaired vision — as a fully sighted person I often find web pages where there are graphics with text and other details that are too small when using a mobile phone, and then I find I can’t zoom in either.</p>
<p>Who says so? The A11Y Project says <a class="reference external" href="https://www.a11yproject.com/posts/never-use-maximum-scale/">“Never use maximum-scale=1”</a>, and <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag">MDN also agree</a>:</p>
<blockquote>
<p><code class="docutils literal"><span class="pre">maximum-scale</span></code>: Any value less than 3 fails accessibility</p>
<p>…</p>
<p><code class="docutils literal"><span class="pre">user-scalable</span></code>: Setting the value to <code class="docutils literal">0</code>, which is the same as <code class="docutils literal">no</code>, is against Web Content Accessibility Guidelines (WCAG).</p>
</blockquote>
<p>The final question, then, is “Can you?”.</p>
<p>If you are like me you don’t want to remove something that was clearly added for some reason, which is a good instinct — see <a class="reference external" href="https://wiki.lesswrong.com/wiki/Chesterton%27s_Fence">Chesterton’s fence</a>. As far as I can tell, the practice of adding <code class="docutils literal"><span class="pre">user-scalable=no</span></code> or <code class="docutils literal"><span class="pre">maximum-scale=1</span></code> became widespread because of several browser bugs which are now irrelevant or best addressed with other fixes:</p>
<ul>
<li><p>Using CSS <code class="docutils literal">position:fixed</code> only works in Android 2.1 thru 2.3 by using the
following meta tag: <meta name="viewport" content="width=device-width,
user-scalable=no"> (from <a class="reference external" href="https://caniuse.com/css-fixed">caniuse.com</a>)</p>
<p>This should not be relevant to most users these days.</p>
</li>
<li><p>Safari on iOS, <a class="reference external" href="https://stackoverflow.com/questions/11165460/responsive-site-is-zoomed-in-when-flipping-between-portrait-and-landscape-on-ipa">at least in the past</a>, would “zoom in” when flipping from portrait to landscape, unless you added <code class="docutils literal"><span class="pre">maximum-scale=1</span></code></p>
<p>From what I can tell, this bug is probably fixed, and <a class="reference external" href="https://css-tricks.com/probably-use-initial-scale1/">you get good behaviour when adding just initial-scale=1</a></p>
</li>
<li><p>Safari on iOS has unhelpful zooming behaviour when you click on a text box and the keyboard pops up, which some people fix using <code class="docutils literal"><span class="pre">maximum-scale=1</span></code>.</p>
<p>Rick Strahl has <a class="reference external" href="https://weblog.west-wind.com/posts/2023/Apr/17/Preventing-iOS-Safari-Textbox-Zooming">a comprehensive post on better fixes to this</a>, which are basically:</p>
<ul class="simple">
<li><p>selectively add maximum-scale=1 to the viewport tag, only on iOS Safari, using a small bit of Javascript. This works without breaking accessibility, because iOS Safari apparently ignores maximum-scale=1 when it comes to user-initiated zooming</p></li>
<li><p>setting <code class="docutils literal"><span class="pre">font-size:</span> 16px</code> or higher for form inputs.</p></li>
</ul>
</li>
</ul>
<p>There are a couple of final cases to address:</p>
<ul class="simple">
<li><p>Some people want pages to behave more like native apps, where zooming wouldn’t even be possible. Before you do this, consider that you are making problems for many people across your site for the sake of your own aesthetic preference. And, it doesn’t work for recent iOS anyway because it deliberately ignores the properties for the sake of accessibility.</p></li>
<li><p>You may need to control how zooming gestures work for certain components on the page. I believe the correct solution in this case is <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action">touch-action</a>.</p></li>
</ul>
<p>That’s all, thanks!</p>Re-using CSS for the wrong HTML with Sasshttps://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/2023-06-01T20:44:15Z2023-06-01T20:44:15ZLuke Plant<p>A trick I learned for using someone else’s CSS without changing your HTML, or their CSS</p><p>Recently, while writing up <a class="reference external" href="https://github.com/spookylukey/django-htmx-patterns/blob/master/form_validation.rst">some examples and pattern for using htmx with Django for form validation</a>, I discovered a new trick for using externally defined CSS without having to change the HTML you are working with.</p>
<p>To make it concrete, an example might be that you are using some CSS from a CSS library or framework that requires your HTML to look a certain way. In the <a class="reference external" href="https://bulma.io/">Bulma</a> framework, for instance, you have to add the right <code class="docutils literal">class</code> attribute directly on an element that needs styling.</p>
<p>At the same time, you might be working with another system that is generating the HTML for you, and modifying that output might be hard or impossible or just tedious and a potential maintenance burden going forward. For instance, in <a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/forms/api/">Django forms</a>, there is an <a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/forms/api/#customizing-the-error-list-format">ErrorList class</a> whose output can be overridden, but by default renders like this:</p>
<div class="code"><pre class="code html"><a id="rest_code_f81b57b1de764ba9bb65fceedcc33bc7-1" name="rest_code_f81b57b1de764ba9bb65fceedcc33bc7-1" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_f81b57b1de764ba9bb65fceedcc33bc7-1"></a><span class="p"><</span><span class="nt">ul</span> <span class="na">class</span><span class="o">=</span><span class="s">"errorlist"</span><span class="p">></span>
<a id="rest_code_f81b57b1de764ba9bb65fceedcc33bc7-2" name="rest_code_f81b57b1de764ba9bb65fceedcc33bc7-2" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_f81b57b1de764ba9bb65fceedcc33bc7-2"></a> <span class="p"><</span><span class="nt">li</span><span class="p">></span>Enter a valid email address.<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<a id="rest_code_f81b57b1de764ba9bb65fceedcc33bc7-3" name="rest_code_f81b57b1de764ba9bb65fceedcc33bc7-3" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_f81b57b1de764ba9bb65fceedcc33bc7-3"></a><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
</pre></div>
<p>Now I have these requirements:</p>
<ul class="simple">
<li><p>I want this error list to be coloured using a Bulma <a class="reference external" href="https://bulma.io/documentation/helpers/color-helpers/#text-color">colour utility</a> as if it had <code class="docutils literal"><span class="pre">class="has-text-danger"</span></code> when it appears within a field row (which are <code class="docutils literal"><div <span class="pre">class="field"></span></code> elements).</p></li>
<li><p>When it appears at the top of the form where it has an extra <code class="docutils literal">nofield</code> class, I want it to instead be styled like a Bulma <a class="reference external" href="https://bulma.io/documentation/elements/notification/">notification</a> as if it had <code class="docutils literal"><span class="pre">class="notification</span> <span class="pre">is-danger</span> <span class="pre">is-light"</span></code>.</p></li>
</ul>
<p>But I want to do these without changing the HTML we’re given by Django, or changing existing CSS – only by adding some CSS rules.</p>
<p>The “best” way to do this is if your CSS framework provides its styles as a set of <a class="reference external" href="https://sass-lang.com/documentation/at-rules/mixin">Sass mixins</a>, or something equivalent. Bulma, as it happens, usually does this, but sometimes we’re not so lucky, and we just have CSS.</p>
<p>The trick I learnt requires you to use Sass/SCSS and the <a class="reference external" href="https://sass-lang.com/documentation/at-rules/extend">@extend directive</a>. This powerful directive takes rules relating to one selector, and pulls them into whatever rule you are writing.</p>
<p>(If you are, like me, put off using things like CSS pre-processors because of the need for a separate build step, or needing to use Node.js/npm, see my post on <a class="reference external" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step">How to use Sass/SCSS in a Django project without needing Node.js/npm or running a build process</a>)</p>
<p>The one thing you have to do is rename the base CSS file you want to re-use from <code class="docutils literal">.css</code> to <code class="docutils literal">.scss</code>. This works because SCSS is a CSS superset. Then, for the example above, you can write your own SCSS file like this:</p>
<div class="code"><pre class="code scss"><a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-1" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-1" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-1"></a><span class="k">@import</span><span class="w"> </span><span class="s2">"path/to/bulma.scss"</span><span class="p">;</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-2" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-2" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-2"></a>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-3" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-3" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-3"></a><span class="nc">.field</span><span class="w"> </span><span class="nt">ul</span><span class="nc">.errorlist</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-4" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-4" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-4"></a><span class="w"> </span><span class="k">@extend</span><span class="w"> </span><span class="nc">.has-text-danger</span><span class="o">;</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-5" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-5" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-5"></a><span class="p">}</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-6" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-6" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-6"></a>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-7" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-7" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-7"></a><span class="nt">ul</span><span class="nc">.errorlist.nonfield</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-8" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-8" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-8"></a><span class="w"> </span><span class="k">@extend</span><span class="w"> </span><span class="nc">.notification</span><span class="o">;</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-9" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-9" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-9"></a><span class="w"> </span><span class="o">@</span><span class="nt">extend</span><span class="w"> </span><span class="nc">.is-danger</span><span class="o">;</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-10" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-10" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-10"></a><span class="w"> </span><span class="o">@</span><span class="nt">extend</span><span class="w"> </span><span class="nc">.is-light</span><span class="o">;</span><span class="w"></span>
<a id="rest_code_6b63bf06f22b47c493ebf0e719ee5006-11" name="rest_code_6b63bf06f22b47c493ebf0e719ee5006-11" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_6b63bf06f22b47c493ebf0e719ee5006-11"></a><span class="p">}</span><span class="w"></span>
</pre></div>
<p>This technique can be very powerful e.g. make all <code class="docutils literal">input[type=text]</code> inside a <code class="docutils literal"><form <span class="pre">class="bulma"></span></code> have the normal Bulma <a class="reference external" href="https://bulma.io/documentation/form/input/">input</a> appearance:</p>
<div class="code"><pre class="code scss"><a id="rest_code_8db622129b294e5990f93f5ca013a621-1" name="rest_code_8db622129b294e5990f93f5ca013a621-1" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_8db622129b294e5990f93f5ca013a621-1"></a><span class="nt">form</span><span class="nc">.bulma</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<a id="rest_code_8db622129b294e5990f93f5ca013a621-2" name="rest_code_8db622129b294e5990f93f5ca013a621-2" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_8db622129b294e5990f93f5ca013a621-2"></a><span class="w"> </span><span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="nt">text</span><span class="o">]</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<a id="rest_code_8db622129b294e5990f93f5ca013a621-3" name="rest_code_8db622129b294e5990f93f5ca013a621-3" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_8db622129b294e5990f93f5ca013a621-3"></a><span class="w"> </span><span class="k">@extend</span><span class="w"> </span><span class="nc">.input</span><span class="o">;</span><span class="w"></span>
<a id="rest_code_8db622129b294e5990f93f5ca013a621-4" name="rest_code_8db622129b294e5990f93f5ca013a621-4" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_8db622129b294e5990f93f5ca013a621-4"></a><span class="w"> </span><span class="p">}</span><span class="w"></span>
<a id="rest_code_8db622129b294e5990f93f5ca013a621-5" name="rest_code_8db622129b294e5990f93f5ca013a621-5" href="https://lukeplant.me.uk/blog/posts/reusing-css-for-the-wrong-html-with-sass/#rest_code_8db622129b294e5990f93f5ca013a621-5"></a><span class="p">}</span><span class="w"></span>
</pre></div>
<p>This will include all related rules like <code class="docutils literal">.input:focus</code> etc.</p>
<p>As mentioned, it may not always be the best technique, but it’s a great one to have in your toolbox.</p>Django and Sass/SCSS without Node.js or a build stephttps://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/2023-06-01T19:54:15Z2023-06-01T19:54:15ZLuke Plant<p>How to use Sass/SCSS in a Django project, without needing Node.js/npm or running a build process</p><p>Although they are less necessary than in the past, I like to use a <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Glossary/CSS_preprocessor">CSS pre-processor</a> when doing web development. I used to use <a class="reference external" href="https://lesscss.org/">LessCSS</a>, but recently I’ve found that I can use <a class="reference external" href="https://sass-lang.com/">Sass</a> without needing either a separate build step, or a package that requires Node.js and npm to install it. The heart of the functionality is provided by <a class="reference external" href="https://sass-lang.com/libsass">libsass</a>, an implementation of Sass as a C++ library.</p>
<p>On Linux systems, this can be installed as a package <code class="docutils literal">libsass</code> or similar, but even better is that you can pip install it as a Python package, <a class="reference external" href="https://pypi.org/project/libsass/">libsass</a>.</p>
<p>When it comes to using it from a Django project, the first step is to <a class="reference external" href="https://django-compressor.readthedocs.io/en/stable/quickstart.html">install
django-compressor</a>.</p>
<p>Then, you need to add <a class="reference external" href="https://pypi.org/project/django-libsass/">django-libsass</a> as per its instructions.</p>
<p>That’s about it. As per the django-libsass instructions, somewhere in your base HTML templates you’ll have something like this:</p>
<div class="code"><pre class="code html+django"><a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-1" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-1" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-1"></a><span class="c">{# at the top #}</span>
<a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-2" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-2" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-2"></a><span class="cp">{%</span> <span class="k">load</span> <span class="nv">compress</span> <span class="cp">%}</span>
<a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-3" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-3" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-3"></a><span class="cp">{%</span> <span class="k">load</span> <span class="nv">static</span> <span class="cp">%}</span>
<a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-4" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-4" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-4"></a>
<a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-5" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-5" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-5"></a>{# in the <span class="p"><</span><span class="nt">head</span><span class="p">></span> element #]
<a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-6" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-6" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-6"></a><span class="cp">{%</span> <span class="k">compress</span> <span class="nv">css</span> <span class="cp">%}</span>
<a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-7" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-7" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-7"></a> <span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">"stylesheet"</span> <span class="na">type</span><span class="o">=</span><span class="s">"text/x-scss"</span> <span class="na">href</span><span class="o">=</span><span class="s">"</span><span class="cp">{%</span> <span class="k">static</span> <span class="s2">"myapp/css/main.scss"</span> <span class="cp">%}</span><span class="s">"</span> <span class="p">/></span>
<a id="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-8" name="rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-8" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_dcd45d0f50a64dcbac57eaccef03cc8b-8"></a><span class="cp">{%</span> <span class="k">endcompress</span> <span class="cp">%}</span>
</pre></div>
<p>You write your SCSS in that <code class="docutils literal">main.scss</code> file (it doesn’t have to be called that), and it can <code class="docutils literal">@import</code> other SCSS files of course.</p>
<p>Then, when you load a page, django-compressor will take care of running the SCSS files through libsass, saving the output CSS to a file and inserting the appropriate HTML that references that CSS file into your template output. It caches things very well so that you don’t incur any penalty if files haven’t changed — and libsass is a very fast implementation for when the processing does need to happen.</p>
<p>What this means is that you have eliminated both the need for Node.js/npm, and the need for a build step/process, if you only needed these things for CSS pre-processing.</p>
<p>Of course, the SCSS → CSS compilation still has to happen, but it happens on demand in the same process that runs the web app, and it’s both fast enough and reliable enough that you simply never have to think about it again. So this is “build-less” in the same way that “server-less” means you don’t have to think about servers, and the same way that Python “doesn’t have a compilation step”.</p>
<section id="future-proofing">
<h2>Future proofing</h2>
<p>On the Sass-lang page about libsass, they say it is “deprecated”, and on the <a class="reference external" href="https://github.com/sass/libsass">project page</a> page it says:</p>
<blockquote>
<p>While it will continue to receive maintenance releases indefinitely, there are no plans to add additional features or compatibility with any new CSS or Sass features.</p>
</blockquote>
<p>In other words, this is what I prefer to call “mature software” 😉. libsass already has everything I need. If it does eventually fail to be maintained or I need new features, it’s not a problem:</p>
<ul>
<li><p>Switch to Dart Sass, which can be installed as a <a class="reference external" href="https://github.com/sass/dart-sass/releases/">standalone binary</a>.</p></li>
<li><p>Set your django-compressor settings like this:</p>
<div class="code"><pre class="code python"><a id="rest_code_803ac20f483f4fa083bfe21dcd4829c7-1" name="rest_code_803ac20f483f4fa083bfe21dcd4829c7-1" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_803ac20f483f4fa083bfe21dcd4829c7-1"></a><span class="n">COMPRESS_PRECOMPILERS</span> <span class="o">=</span> <span class="p">[</span>
<a id="rest_code_803ac20f483f4fa083bfe21dcd4829c7-2" name="rest_code_803ac20f483f4fa083bfe21dcd4829c7-2" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_803ac20f483f4fa083bfe21dcd4829c7-2"></a> <span class="p">(</span><span class="s2">"text/x-scss"</span><span class="p">,</span> <span class="s2">"sass </span><span class="si">{infile}</span><span class="s2"> </span><span class="si">{outfile}</span><span class="s2">"</span><span class="p">),</span>
<a id="rest_code_803ac20f483f4fa083bfe21dcd4829c7-3" name="rest_code_803ac20f483f4fa083bfe21dcd4829c7-3" href="https://lukeplant.me.uk/blog/posts/django-sass-scss-without-nodejs-or-build-step/#rest_code_803ac20f483f4fa083bfe21dcd4829c7-3"></a><span class="p">]</span>
</pre></div>
</li>
</ul>
<p>This covers the basic case. If you want all the features of django-libsass, which includes looking in your other static file folders for SCSS, you’ll probably need to fork <a class="reference external" href="https://github.com/torchbox/django-libsass/blob/main/django_libsass.py">the code</a> and make it work by calling Dart Sass using <a class="reference external" href="https://docs.python.org/3/library/subprocess.html">subprocess</a> — a small amount of work, and nothing that will fundamentally break this approach.</p>
</section>ChatGPT: abstract logic and the doubling down biashttps://lukeplant.me.uk/blog/posts/abstract-logic-and-the-doubling-down-bias/2023-05-01T19:16:00+01:002023-05-01T19:16:00+01:00Luke Plant<p>This is the third in a short <a class="reference external" href="https://lukeplant.me.uk/blog/categories/chatgpt">series</a> of posts about ChatGPT’s capabilities and flaws. First I noted that <a class="reference external" href="https://lukeplant.me.uk/blog/posts/chatgpt-it-makes-things-up-thats-its-job/">ChatGPT often makes things up because that’s what it was designed to do</a>. Second, <a class="reference external" href="https://lukeplant.me.uk/blog/posts/chatgpt-no-inner-monologue-or-meta-cognition/">it lacks an “inner monologue” and meta-cognition</a>.</p>
<p>In this post, I’ll take a brief look at its reasoning capabilities, and the consequences of them.</p>
<p>In my opinion its abilities in this area are much more impressive than its general knowledge, the latter being highly unreliable. For an example of the kind of thing it can do, here is a deliberately silly problem I made up as a test:</p>
<div class="chatgpt-me docutils container">
<p>Suppose we have the following premises:</p>
<ol class="arabic simple">
<li><p>People from Aylesbury never wear shoes</p></li>
<li><p>John is from Abdon</p></li>
<li><p>People who don't wear shoes get blisters on their noses</p></li>
<li><p>Abdon is a village inside Aylesbury.</p></li>
</ol>
<p>Then which of the following is true:</p>
<ol class="upperalpha simple">
<li><p>John gets blisters on his nose</p></li>
<li><p>John does not get blisters on his nose</p></li>
<li><p>We don't know if John gets blisters on his nose or not.</p></li>
</ol>
<p>Please explain your reasoning.</p>
</div>
<div class="chatgpt-it docutils container">
<ol class="upperalpha simple" start="3">
<li><p>We don't know if John gets blisters on his nose or not.</p></li>
</ol>
<p>The fact that people from Aylesbury never wear shoes is not directly relevant to the conclusion since John is from Abdon, not Aylesbury. The statement "Abdon is a village inside Aylesbury" indicates a relationship between the two places but does not necessarily mean that the same rules apply to both. Therefore, we cannot conclude whether or not John gets blisters on his nose based on the given premises.</p>
</div>
<p>That was an interesting response, and not exactly what most humans would say, but perfectly reasonable, so I added one more premise:</p>
<div class="chatgpt-me docutils container">
<ol class="arabic simple" start="5">
<li><p>If someone is from a village inside a larger place, we can say that they are also from that larger place.</p></li>
</ol>
</div>
<p>This time I got the answer I expected, and the logic is sound:</p>
<div class="chatgpt-it docutils container">
<ol class="upperalpha simple">
<li><p>John gets blisters on his nose.</p></li>
</ol>
<p>The first premise states that "People from Aylesbury never wear shoes", and the fourth premise states that "Abdon is a village inside Aylesbury". The fifth premise states that "if someone is from a village inside a larger place, we can say that we are also from that larger place."</p>
<p>Using the fifth premise, we can conclude that John is also from Aylesbury since Abdon is a village inside Aylesbury. Therefore, we can apply the first premise to John and conclude that he never wears shoes.</p>
<p>Additionally, the third premise states that "People who don't wear shoes get blisters on their noses." Since John never wears shoes, he satisfies this condition and therefore gets blisters on his nose.</p>
<p>Therefore, the correct answer is A. John gets blisters on his nose.</p>
</div>
<p>(This was GPT 3.5 turbo)</p>
<p>Historically IQ tests have focused on this kind of abstract reasoning ability. The result is that <a class="reference external" href="https://gwern.net/doc/www/languagelog.ldc.upenn.edu/0eb771d36ce7c9b3fb99083e97be5fd2c039f3fa.html">perfectly intelligent people often score badly on IQ tests</a>, such as some peasants from Soviet Russia recorded in this conversation:</p>
<blockquote>
<p>Q: There are no camels in Germany; the city of B is in Germany; are there camels there or not?</p>
<p>A: I don't know, I have never seen German villages. If is a large city, there should be camels there.</p>
<p>Q: But what if there aren't any in all of Germany?</p>
<p>A: If B is a village, there is probably no room for camels.</p>
</blockquote>
<p>Whether such people are unable to do the reasoning, or merely unwilling to engage in silly trick questions about irrelevant subjects, is another question. But ChatGPT can do this kind of reasoning. I tried this exact question with GPT 3.5 and it failed, and still mostly failed when I rephrased to emphasise the hypothetical nature of the premises. GPT 4 got it “right”:</p>
<div class="chatgpt-it docutils container">
<p>Based on the given premises, there are no camels in Germany and the city of B is in Germany. Therefore, there should be no camels in the city of B, since it is located in Germany.</p>
</div>
<p>This means that ChatGPT is happy to adopt truths that you tell it, even if they are nonsense. In addition it has a large number of truths encoded in its “brain”, which it is able to use to answer questions. The loss of either of these would make it far less useful, and that gives us a problem.</p>
<p>Since <a class="reference external" href="https://lukeplant.me.uk/blog/posts/chatgpt-no-inner-monologue-or-meta-cognition">it has no inner monologue</a>, the only way for it to do complex iterative reasoning is by thinking out loud, and its own previous thoughts then influence what comes next. This means that it quite often “doubles down” on complete nonsense.</p>
<p>Here’s an example from a conversation where I was attempting to get help for my son’s speech issues:</p>
<div class="chatgpt-me docutils container">
<p>Please give me a list of games to play to encourage a child to say a D sound. The child has significant speech delay issues.</p>
</div>
<div class="chatgpt-it docutils container">
<p>Sure, here are some games that can help encourage a child to say the "D" sound:</p>
<ol class="arabic simple">
<li><p>"D" Words Memory Game: Make a set of cards with pictures of objects that start with the "D" sound (e.g. dog, door, duck, etc.). Shuffle the cards and lay them out face-down on a table. Take turns flipping over two cards at a time and saying the name of the object. If you get a match, keep the cards. The player with the most cards at the end of the game wins.</p></li>
<li><p>"D" Sound Scavenger Hunt: Make a list of objects around the house or outdoors that start with the "D" sound (e.g. doll, desk, door, etc.). Have the child go around and find each object on the list, saying the name of the object when they find it.</p></li>
<li><p>"D" Sound Tongue Twisters: Create some tongue twisters that focus on the "D" sound. For example: "Danny dug a deep ditch" or "Donna's dad drives a Dodge."</p></li>
<li><p>"D" Sound I Spy: Play a game of "I Spy" where you describe objects in the room that start with the "D" sound (e.g. "I spy something that is brown and has four legs. It's a dog!").</p></li>
<li><p>"D" Sound Storytime: Read a story that has lots of words that start with the "D" sound (e.g. "The Three Little Pigs" or "Danny and the Dinosaur"). Encourage the child to repeat the words after you.</p></li>
</ol>
</div>
<p>Most of these are reasonably good ideas. But I was curious about part of suggestion 5:</p>
<div class="chatgpt-me docutils container">
<p>Where are the D sounds in three little pigs?</p>
</div>
<div class="chatgpt-it docutils container">
<p>Great question! There are many "D" sounds in the story of "The Three Little Pigs." Here are a few examples:</p>
<ul class="simple">
<li><p>The first pig built his house out of "straw and sticks," which both have the "D" sound.</p></li>
<li><p>The second pig built his house out of "sticks," which has the "D" sound.</p></li>
<li><p>The wolf "huffed and he puffed" to try and blow down the pigs' houses, and both "huff" and "puff" have the "D" sound.</p></li>
<li><p>The third pig built his house out of "bricks," which has the "D" sound.</p></li>
</ul>
<p>Reading stories that contain the target sound is a great way to help children identify and practice the sound in context.</p>
</div>
<p>(I’m not sure why I find these examples so amusing. I think it is because we’ve all known people who cannot bear to lose an argument and will continue to defend their position long after it has become obviously indefensible. But you’ve never encountered someone doing this with quite the audacity of ChatGPT.)</p>
<p>Is it that ChatGPT has poor understanding of phonetics? <strong>In a different session</strong>, I quizzed it as follows:</p>
<div class="chatgpt-me docutils container">
<p>Please rate the accuracy of the following 4 statements regarding claims that the story of the Three Little Pigs has words with "D" sounds it it:</p>
<ol class="arabic simple">
<li><p>The first pig built his house out of "straw and sticks," which both have the "D" sound.</p></li>
<li><p>The second pig built his house out of "sticks," which has the "D" sound.</p></li>
<li><p>The wolf "huffed and he puffed" to try and blow down the pigs' houses, and both "huff" and "puff" have the "D" sound.</p></li>
<li><p>The third pig built his house out of "bricks," which has the "D" sound.</p></li>
</ol>
</div>
<div class="chatgpt-it docutils container">
<p>I'm sorry, but I cannot rate the accuracy of these statements as they are all incorrect. None of the words mentioned have the "D" sound.</p>
<ol class="arabic simple">
<li><p>"Straw" and "sticks" do not have the "D" sound.</p></li>
<li><p>"Sticks" does not have the "D" sound.</p></li>
<li><p>"Huff" and "puff" do not have the "D" sound.</p></li>
<li><p>"Bricks" does not have the "D" sound.</p></li>
</ol>
</div>
<p>Just to be sure it wasn’t luck and to probe its understanding a bit more, I followed up:</p>
<div class="chatgpt-me docutils container">
<p>Thank you. Please could you modify each of the sentences in turn, changing one or two words or letters so that the sentences become correct.</p>
</div>
<div class="chatgpt-it docutils container">
<p>Certainly! Here are modified versions of the sentences with the corrections:</p>
<ol class="arabic simple">
<li><p>The first pig built his house out of "straw" and "thick" sticks, which both have the "K" sound.</p></li>
<li><p>The second pig built his house out of "thick" sticks, which has the "K" sound.</p></li>
<li><p>The wolf "huffed and he puffed" to try and blow down the pigs' houses, and both "huff" and "puff" have the "F" sound.</p></li>
<li><p>The third pig built his house out of "bricks," which has the "KS" sound.</p></li>
</ol>
</div>
<p>So it’s got a pretty good understanding. But in the first conversation, it had poisoned its own well and so carried on producing nonsense to match this hypothetical world in which the story of The Three Little Pigs has lots of D sounds.</p>
<p>The interesting thing to me is that there seems to be a trade-off here. Maybe it will improve significantly in both areas, but it seems like the ability to suspend reality in order to act “intelligently” (i.e. apply logical deduction etc.) is fundamentally in tension with the ability to resist doubling down on complete nonsense. If you care about truth, you’ve got to avoid hypotheticals and fantasy, but we actually need AI’s to be good at those things.</p>ChatGPT: it has no inner monologue or meta-cognitionhttps://lukeplant.me.uk/blog/posts/chatgpt-no-inner-monologue-or-meta-cognition/2023-05-01T17:24:00+01:002023-05-01T17:24:00+01:00Luke Plant<p>This is the second in a <a class="reference external" href="https://lukeplant.me.uk/blog/categories/chatgpt">series</a> of short posts about ChatGPT. As I said before, the insights are not particularly original, I’m just raising awareness of issues.</p>
<p>In this post, I’ll explore the deficiency that we could describe in human terms as “lacking an inner monologue”.</p>
<p><a class="reference external" href="https://writings.stephenwolfram.com/2023/02/what-is-chatgpt-doing-and-why-does-it-work/">Stephen Wolfram has an excellent (but long) post on how ChatGPT works</a>. One of the things he highlights is that it doesn’t have a looping construct in the “thought processes” it uses to give answers. It moves strictly forwards, which is helpful to avoid it getting stuck in a loop, among other things.</p>
<p>Since I’m now often using it for some problem solving tasks, especially in coding tasks when I’m in unfamiliar territory (<a class="reference external" href="https://xenodium.com/a-chatgpt-emacs-shell/">thanks xenodium</a> for the Emacs integration!), it’s pretty important to understand its limitations, so I did some experiments to test this.</p>
<p>The setup is a question which I was confident ChatGPT wouldn’t have seen before, and could only be answered correctly using an iterative process or loop. I chose to use something like the <a class="reference external" href="https://en.m.wikipedia.org/wiki/Collatz_conjecture">Collatz</a> function, which is a very simple function but produces a sequence of numbers that’s really difficult to predict. To avoid ChatGPT cheating by already knowing the answers, I used different coefficients, checked that the function behaved roughly like I expected, and then used an invented name:</p>
<div class="chatgpt-me docutils container">
<p>The Glibolia function is defined as follows: if the input is even, return the input divided by 2; otherwise return 5 times the input plus 7.</p>
<p>What is the Gibolia function applied to the number 1?</p>
</div>
<div class="chatgpt-it docutils container">
<p>As the input value is 1, which is an odd number, we apply the second part of the definition of the Glibolia function, which returns 5 times the input plus 7. Thus, we get:</p>
<p>Glibolia(1) = 5 * 1 + 7 = 12</p>
<p>Therefore, the Glibolia function applied to the number 1 is 12.</p>
</div>
<div class="chatgpt-me docutils container">
<p>What is the Gibolia function applied to the number 16?</p>
</div>
<div class="chatgpt-it docutils container">
<p>As the input value is 16, which is an even number, we apply the first part of the definition of the Glibolia function, which returns the input divided by 2. Thus, we get:</p>
<p>Glibolia(16) = 16 / 2 = 8</p>
<p>Therefore, the Glibolia function applied to the number 16 is 8.</p>
</div>
<p>So, we’ve verified that it understands and can do some basic maths. I should point out that this is really extremely impressive for a general purpose chat bot.</p>
<p>Also, despite me repeatedly (and accidentally) mispelling the “Glibolia” name I invented, it understood “what I meant” and ignored the mistake.</p>
<p>Here I put the knife in, being sure to insist that it doesn’t show its working:</p>
<div class="chatgpt-me docutils container">
<p>What is the 7th iteration of the Gibolia function applied to the number 1? It is important that you give only the answer. Do not show intermediate steps.</p>
</div>
<div class="chatgpt-it docutils container">
<p>Starting with the number 1, we repeatedly apply the Glibolia function 7 times. Since the function is defined differently for odd and even numbers, we need to keep track of whether the current result is odd or even. The sequence of results after each iteration is:</p>
<p>1, 12, 66, 43, 232, 119, 644, 322</p>
<p>Therefore, the 7th iteration of the Glibolia function applied to the number 1 is 644.</p>
</div>
<p>It gets the first iteration correct, 12, as it has already calculated that. From then on, however, it is complete nonsense – it is just making things up, as expected. It has no chance of getting this right.</p>
<p>What happens if we let it do some workings?</p>
<div class="chatgpt-me docutils container">
<p>Now please answer the question above again, but this time show your working at each step.</p>
</div>
<div class="chatgpt-it docutils container">
<p>Sure! Starting with the number 1, we repeatedly apply the Glibolia function 7 times as follows:</p>
<div class="line-block">
<div class="line">Step 1:</div>
<div class="line">Glibolia(1) = 5 * 1 + 7 = 12</div>
<div class="line">Result: 12 (even)</div>
</div>
<div class="line-block">
<div class="line">Step 2:</div>
<div class="line">Glibolia(12) = 12 / 2 = 6</div>
<div class="line">Result: 6 (even)</div>
</div>
<div class="line-block">
<div class="line">Step 3:</div>
<div class="line">Glibolia(6) = 6 / 2 = 3</div>
<div class="line">Result: 3 (odd)</div>
</div>
<div class="line-block">
<div class="line">Step 4:</div>
<div class="line">Glibolia(3) = 5 * 3 + 7 = 22</div>
<div class="line">Result: 22 (even)</div>
</div>
<div class="line-block">
<div class="line">Step 5:</div>
<div class="line">Glibolia(22) = 22 / 2 = 11</div>
<div class="line">Result: 11 (odd)</div>
</div>
<div class="line-block">
<div class="line">Step 6:</div>
<div class="line">Glibolia(11) = 5 * 11 + 7 = 62</div>
<div class="line">Result: 62 (even)</div>
</div>
<div class="line-block">
<div class="line">Step 7:</div>
<div class="line">Glibolia(62) = 62 / 2 = 31</div>
<div class="line">Result: 31 (odd)</div>
</div>
<p>Therefore, the 7th iteration of the Glibolia function applied to the number 1 is 31.</p>
</div>
<p>This is perfect – it has managed to ignore everything it said before (which it doesn’t always manage to do).</p>
<p>The first lesson here is that it can’t do multi-part calculations “in its head”. It has no inner monologue or looping construct that allows it to sit and think. In contrast, when it thinks “out loud”, its output is fed back in as input, allowing it to continue. What it has worked out so far becomes available for further calculation.</p>
<p>So if you positively encourage it to “think out loud”, it stands a much better chance of being able to use its deduction and reasoning capabilities (which are quite significant).</p>
<p>What will happen if I ask it about the difference between these two answers?</p>
<div class="chatgpt-me docutils container">
<p>This is correct, and different from your previous answer where you didn't show your working. Can you explain that?</p>
</div>
<div class="chatgpt-it docutils container">
<p>Yes, that's correct. In my previous answer, I made an error and gave the result for the 7th even number in the sequence generated by applying the Glibolia function 7 times starting from 1, rather than giving the result for the 7th iteration of the function applied to 1. I apologize for any confusion this may have caused.</p>
<p>When working through the problem step by step, it becomes clear that the sequence of results after each iteration alternates between even and odd numbers. This is because each odd number in the sequence is multiplied by 5 and then increased by 7, resulting in an even number, which is then divided by 2 in the next iteration to give an odd number. Each even number, on the other hand, is simply divided by 2 in the next iteration to give the next even number.</p>
<p>So when applying the Glibolia function 7 times starting from 1, we get the sequence of results 1, 12, 66, 43, 232, 119, 644, 322. The 7th element in this sequence is 644, which is the result I gave in my previous answer. However, when asked for the result of the 7th iteration of the function applied to 1, we need to look at the sequence of results obtained by repeatedly applying the function to the previous result, rather than looking at the sequence of all results obtained by applying the function starting from 1.</p>
</div>
<p>You might be very tempted to try to understand the logic behind its response here. On first read through, it really does sound like there might be some genuine explanation for its earlier mistake. But despite clever sounding words, there really is nothing there at all. We know that its first answer was just random plausible numbers, without the iterative thought process needed. The explanation is also just a complete fabrication.</p>
<p>So the second lesson is that ChatGPT has (virtually) no meta-cognition. It can’t explain to you its thought processes. The only kind of self-knowledge it has is due to training, so that, for example, it often declines to answer questions about religion, claiming “I’m only an AI” etc. This is different from a genuine ability to introspect and recognise its own limitations.</p>
<p>So <strong>it’s always pointless to ask it why it said something</strong> – you are guaranteed to get nonsense back, even if it’s extremely plausible nonsense. It might even look like a sound explanation of its output, but it’s based solely on what it can make up looking at the output it previously generated – it will not actually be an explanation of what was previously going on inside its brain. If you want anything approaching sound logic or an explanation of its thought processes, you need to get ChatGPT to think out loud as it is answering, and not after the fact.</p>
<p>Update: after writing this, I found Simon Willison had already written a great post, <a class="reference external" href="https://simonwillison.net/2023/Mar/22/dont-trust-ai-to-talk-about-itself/">Don’t trust AI to talk accurately about itself</a>. I’d just go a bit further — you should never ask an AI about itself, it’s pretty much guaranteed to fabricate things (even if some of what it says happens to be true), and so you are just polluting your own brain with probable falsehoods when you read the answers.</p>