
    
        
        
    
                
        
        
        
                
        
        
        
                
        
        
        
                
        
        
        
                
        
        
        
                
        
        
        
                
        
        
        
                
        
        
        
                
        
        
        
            
{"version":"https:\/\/jsonfeed.org\/version\/1","title":"mathspp.com feed","home_page_url":"https:\/\/mathspp.com\/blog\/til","feed_url":"https:\/\/mathspp.com\/blog\/til.json","description":"Stay up-to-date with the articles on mathematics and programming that get published to mathspp.com.","author":{"name":"Rodrigo Gir\u00e3o Serr\u00e3o"},"items":[{"title":"TIL #145 \u2013 collections.deque is implemented in blocks","date_published":"2026-05-20T09:25:00+02:00","id":"https:\/\/mathspp.com\/blog\/til\/collections-deque-is-implemented-in-blocks","url":"https:\/\/mathspp.com\/blog\/til\/collections-deque-is-implemented-in-blocks","content_html":"<p>Today I learned that <code>collections.deque<\/code> is implemented as a doubly-linked list of blocks.<\/p>\n\n<h2 id=\"collections-deque\"><code>collections.deque<\/code><a href=\"#collections-deque\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>I've written about the data structure <code>deque<\/code> from the module <code>collections<\/code> extensively.\nIn particular, I wrote a <a href=\"\/blog\/python-deque-tutorial\"><code>deque<\/code> tutorial with plenty of practical example use cases of <code>deque<\/code><\/a>.<\/p>\n<p>Today, after some discussion during a cohort I was teaching, a student <a href=\"https:\/\/github.com\/python\/cpython\/blob\/d948eaa366029bc358dbe9cf32d545c3ad30c502\/Modules\/_collectionsmodule.c#L82-L127\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" class=\"external-link no-image\">sent a link to the <code>collections.deque<\/code> source code<\/a> where a comment explains some of the lower-level details of how a <code>deque<\/code> is implemented.<\/p>\n<h2 id=\"double-ended-queue\">Double-ended queue<a href=\"#double-ended-queue\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>The name &ldquo;deque&rdquo; stands for Double-Ended QUEue and that's because a deque is a doubly-linked list.\nWhen you learn about that, you might think that, under the hood, a <code>deque<\/code> is essentially a collection of nodes that link to the next and to the previous:<\/p>\n<pre><code class=\"language-py\">class Node:\n    prev_node: Node | None\n    value: object\n    next_node: Node | None\n\nclass deque:\n    first_node: Node | None\n    last_node: Node | None\n    ...<\/code><\/pre>\n<p>But that's not the case.\nApparently, a <code>deque<\/code> is implemented in a more optimised way, where each &ldquo;node&rdquo; is actually a block that can hold up to a certain number of elements.\nAt the level of C, this means you need to manipulate memory less often, which makes the <code>deque<\/code> faster.<\/p>\n<h2 id=\"pseudo-implementation-of-deque-in-python\">Pseudo-implementation of <code>deque<\/code> in Python<a href=\"#pseudo-implementation-of-deque-in-python\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>The Python code below is a pseudo-implementation of <code>deque<\/code> that mimics more or less the underlying block mechanism that's in place for Python 3.15 (and that <em>has been in place for decades<\/em>).\nThe Python code below is stripped of all of the memory operations and other lower-level details and instead focuses on the mechanics of managing blocks:<\/p>\n<pre><code class=\"language-py\">from dataclasses import dataclass, field\n\nBLOCKLEN = 64\nCENTRE = (BLOCKLEN - 1) \/\/ 2\n\ndef new_empty_block_data() -&gt; list[object]:\n    return [None for _ in range(BLOCKLEN)]\n\n@dataclass\nclass Block:\n    left_link: Block | None = None\n    data: list[object] = field(default_factory=new_empty_block_data)\n    right_link: Block | None = None\n\n@dataclass(init=False)\nclass deque:\n    left_block: Block\n    right_block: Block\n    left_index: int\n    right_index: int\n    maxlen: int\n\n    def __init__(self, maxlen: int | None = None) -&gt; None:\n        self.left_block = self.right_block = Block()\n        self.left_index = CENTRE + 1\n        self.right_index = CENTRE\n        if maxlen is None:\n            maxlen = -1\n        self.maxlen = maxlen<\/code><\/pre>\n<p>The two classes <code>Block<\/code> and <code>deque<\/code> set the structure for the <code>deque<\/code>.\nThe attributes <code>left_block<\/code> and <code>right_block<\/code> point, respectively, to the leftmost block and the rightmost block, and the first item of a deque <code>d<\/code> is always found at <code>d.left_block[d.left_index]<\/code> while the last item is at <code>d.right_block[d.right_index]<\/code>.<\/p>\n<p>With this in mind, adding or removing elements from a deque is a matter of managing the blocks and the indices correctly.\nBelow, you can find the pseudo-implementation of <code>append<\/code> and <code>pop<\/code>:<\/p>\n<pre><code class=\"language-py\">@dataclass\nclass deque:\n    ...\n\n    def append(self, item: object) -&gt; None:\n        # If there's no space, create a new block.\n        if self.right_index == BLOCKLEN - 1:\n            new_block = Block()\n            self.right_block.right_link = new_block\n            new_block.left_link = self.right_block\n            self.right_block = new_block\n            self.right_index = -1\n\n        self.right_index += 1\n        self.right_block[self.right_index] = item\n\n        if self.maxlen &gt; -1 and len(self) &gt; self.maxlen:\n            self.popleft()\n\n    def pop(self) -&gt; object:\n        item = self.right_block[self.right_index]\n        self.right_index -= 1\n\n        #...<\/code><\/pre>","summary":"Today I learned that collections.deque is implemented as a doubly-linked list of blocks.","date_modified":"2026-05-20T10:54:10+02:00","image":"\/user\/pages\/02.blog\/04.til\/145.collections-deque-is-implemented-in-blocks\/thumbnail.webp"},{"title":"TIL #144 \u2013 Sentinel built-in","date_published":"2026-05-01T19:49:00+02:00","id":"https:\/\/mathspp.com\/blog\/til\/sentinel-builtin","url":"https:\/\/mathspp.com\/blog\/til\/sentinel-builtin","content_html":"<p>Today I learned Python 3.15 will get a new sentinel built-in.<\/p>\n\n<p>Sentinel values are unique placeholder values that are commonly used in programming.\nPython 3.15 ships with a new built-in <code>sentinel<\/code> that can be used to create new sentinel values:<\/p>\n<pre><code class=\"language-py\"># Python 3.15+\n&gt;&gt;&gt; MISSING = sentinel(\"MISSING\")\n&gt;&gt;&gt; MISSING\nMISSING<\/code><\/pre>\n<p>Before this built-in was added, the most common sentinel idiom used the built-in <code>object<\/code>:<\/p>\n<pre><code class=\"language-py\">MISSING = object()\n\ndef my_function(some_arg=MISSING):\n    if some_arg is MISSING:\n        ... # Handle the sentinel<\/code><\/pre>\n<p>In the function above, the sentinel value <code>MISSING<\/code> is being used to check whether the user passed <em>anything<\/em> as the parameter <code>some_arg<\/code> or not.\n<a href=\"https:\/\/peps.python.org\/pep-0661\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" class=\"external-link no-image\">PEP 661<\/a>, that introduced this built-in, has a great discussion covering the reasons as to why this pattern, and many other sentinel patterns, fall short.\nIn general, each common sentinel idiom suffers from at least one of the following problems:<\/p>\n<ol>\n<li><strong>Bad string repr<\/strong>: the <a href=\"\/blog\/pydonts\/str-and-repr\">string representation<\/a> is too long and uninformative<\/li>\n<li><strong>Type unsafe<\/strong>: the sentinels don't have a distinct type so it becomes hard or impossible to write code that uses the sentinels and is type safe<\/li>\n<li><strong>Unexpected copy behaviour<\/strong>: the sentinels can't be copied or pickled without breaking the sentinel behaviour<\/li>\n<\/ol>","summary":"Today I learned Python 3.15 will get a new sentinel built-in.","date_modified":"2026-05-01T21:18:58+02:00","tags":["programming","python"],"image":"\/user\/pages\/02.blog\/04.til\/144.sentinel-builtin\/thumbnail.webp"},{"title":"TIL #143 \u2013 Resolve a lazy import manually","date_published":"2026-04-27T17:18:00+02:00","id":"https:\/\/mathspp.com\/blog\/til\/resolve-a-lazy-import-manually","url":"https:\/\/mathspp.com\/blog\/til\/resolve-a-lazy-import-manually","content_html":"<p>Learn how to work around the Python machinery to resolve an explicit lazy import manually.<\/p>\n\n<p>A couple of articles ago I wrote about how you could <a href=\"\/blog\/til\/inspect-a-lazy-import\">inspect a lazy import<\/a>.<\/p>\n<p>Apparently, you can use a similar trick to check the attributes and methods that a lazy import has:<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; lazy import json\n&gt;&gt;&gt; dir(globals()[\"json\"])\n['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'resolve']<\/code><\/pre>\n<p>Apart from a large number of <a href=\"\/blog\/pydonts\/dunder-methods\">dunder methods<\/a> and dunder attributes, you'll find the method <code>resolve<\/code>.\nYou can run <code>help(globals()[\"json\"].resolve)<\/code> to get the help text on that method:<\/p>\n<pre><code class=\"language-text\">Help on built-in function resolve:\n\nresolve() method of builtins.lazy_import instance\n    resolves the lazy import and returns the actual object<\/code><\/pre>\n<p>This shows that it's the method <code>resolve<\/code> that resolves a lazy import.<\/p>\n<p>If you call the method, you can get access to the resolved module:<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; lazy import json\n&gt;&gt;&gt; resolved_json = globals()[\"json\"].resolve()\n&gt;&gt;&gt; resolved_json\n&lt;module 'json' from '\/Users\/rodrigogs\/.local\/share\/uv\/python\/cpython-3.15.0a8-macos-aarch64-none\/lib\/python3.15\/json\/__init__.py'&gt;<\/code><\/pre>\n<p>After calling <code>resolve<\/code>, the lazy module doesn't disappear automatically:<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; globals()[\"json\"]\n&lt;lazy_import 'json'&gt;<\/code><\/pre>\n<p>Which shows that the mechanism that's responsible for reification <em>most likely<\/em> calls the method <code>resolve<\/code> and then <em>reassigns<\/em> the name of the module to the module returned by <code>resolve<\/code>.\nIn a way, it's as if the reification process ran something like<\/p>\n<pre><code class=\"language-py\">globals()[\"json\"] = globals()[\"json\"].resolve()<\/code><\/pre>\n<p>In hindsight, this isn't too surprising.\nAfter all, Python tends to be very consistent.\nThe only mistery that remains is <em>what<\/em> triggers the reification process.\nHow is it that Python can detect when something <em>touches<\/em> the lazy import..?<\/p>","summary":"Learn how to work around the Python machinery to resolve an explicit lazy import manually.","date_modified":"2026-04-27T18:29:12+02:00","tags":["programming","python"],"image":"\/user\/pages\/02.blog\/04.til\/143.resolve-a-lazy-import-manually\/thumbnail.webp"},{"title":"TIL #142 \u2013 Cyclic quadrilateral","date_published":"2026-03-14T14:58:00+01:00","id":"https:\/\/mathspp.com\/blog\/til\/cyclic-quadrilateral","url":"https:\/\/mathspp.com\/blog\/til\/cyclic-quadrilateral","content_html":"<p>Today I learned that cyclic quadrilaterals have supplementary opposite angles.<\/p>\n\n<p>A <strong>cyclic quadrilateral<\/strong> \u2014 a quadrilateral whose four vertices all lie on a single circle \u2014 has supplementary opposite angles.<\/p>\n<p>This means that opposite angles add to 180 degrees, or <span class=\"mathjax mathjax--inline\">\\(\\pi\\)<\/span> radians.<\/p>\n<p>As it turns out, this is actually an equivalence relation.\nIf a quadrilateral has supplementary opposite angles, it's a cyclic quadrilateral.<\/p>\n<p>This fact about supplementary opposite angles was very useful for an animation I was trying to create...\nI may share it here later!<\/p>","summary":"Today I learned that cyclic quadrilaterals have supplementary opposite angles.","date_modified":"2026-03-14T16:03:32+01:00","tags":["mathematics","geometry"],"image":"\/user\/pages\/02.blog\/04.til\/142.cyclic-quadrilateral\/thumbnail.webp"},{"title":"TIL #141 \u2013 Inspect a lazy import","date_published":"2026-03-13T14:38:00+01:00","id":"https:\/\/mathspp.com\/blog\/til\/inspect-a-lazy-import","url":"https:\/\/mathspp.com\/blog\/til\/inspect-a-lazy-import","content_html":"<p>Today I learned how to inspect a lazy import object in Python 3.15.<\/p>\n\n<p>Python 3.15 comes with lazy imports and today I played with them for a minute.\nI defined the following module <code>mod.py<\/code>:<\/p>\n<pre><code class=\"language-py\">print(\"Hey!\")\n\ndef f():\n    return \"Bye!\"<\/code><\/pre>\n<p>Then, in the REPL, I could check that lazy imports indeed work:<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; # Python 3.15\n&gt;&gt;&gt; lazy import mod\n&gt;&gt;&gt;<\/code><\/pre>\n<p>The fact that I didn't see a \"Hey!\" means that the import is, indeed, lazy.\nThen, I wanted to take a look at the module so I printed it, but that triggered reification (going from a lazy import to a regular module):<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; print(mod)\nHey!\n&lt;module 'mod' from '\/Users\/rodrigogs\/Documents\/tmp\/mod.py'&gt;<\/code><\/pre>\n<p>So, I checked <a href=\"https:\/\/peps.python.org\/pep-0810\/#reification\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" class=\"external-link no-image\">the PEP that introduced explicit lazy modules<\/a> and turns out as soon as you <em>reference<\/em> the lazy object directly, it gets reified.\nBut you can work around it by using <code>globals<\/code>:<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; # Fresh 3.15 REPL\n&gt;&gt;&gt; lazy import mod\n&gt;&gt;&gt; globals()[\"mod\"]\n&lt;lazy_import 'mod'&gt;<\/code><\/pre>\n<p>This shows the new class <code>lazy_import<\/code> that was added to support lazy imports!<\/p>\n<p>Pretty cool, right?<\/p>","summary":"Today I learned how to inspect a lazy import object in Python 3.15.","date_modified":"2026-03-13T15:54:14+01:00","tags":["programming","python"],"image":"\/user\/pages\/02.blog\/04.til\/141.inspect-a-lazy-import\/thumbnail.webp"},{"title":"TIL #140 \u2013 Install Jupyter with uv","date_published":"2026-03-03T16:16:00+01:00","id":"https:\/\/mathspp.com\/blog\/til\/install-jupyter-with-uv","url":"https:\/\/mathspp.com\/blog\/til\/install-jupyter-with-uv","content_html":"<p>Today I learned how to install jupyter properly while using uv to manage tools.<\/p>\n\n<h2 id=\"running-a-jupyter-notebook-server-or-jupyter-lab\">Running a Jupyter notebook server or Jupyter lab<a href=\"#running-a-jupyter-notebook-server-or-jupyter-lab\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>To run a Jupyter notebook server with uv, you can run the command<\/p>\n<pre><code class=\"language-bash\">$ uvx jupyter notebook<\/code><\/pre>\n<p>Similarly, if you want to run Jupyter lab, you can run<\/p>\n<pre><code class=\"language-bash\">$ uvx jupyter lab<\/code><\/pre>\n<p>Both work, but uv will kindly present a message explaining how it's actually doing you a favour, because it <em>guessed<\/em> what you wanted.\nThat's because <code>uvx something<\/code> usually looks for a package named \u201csomething\u201d with a command called \u201csomething\u201d.<\/p>\n<p>As it turns out, the command <code>jupyter<\/code> comes from the package <code>jupyter-core<\/code>, not from the package <code>jupyter<\/code>.<\/p>\n<h2 id=\"installing-jupyter\">Installing Jupyter<a href=\"#installing-jupyter\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>If you're running Jupyter notebooks often, you can install the notebook server and Jupyter lab with<\/p>\n<pre><code class=\"language-bash\">$ uv tool install --with jupyter jupyter-core<\/code><\/pre>\n<h3 id=\"why-uv-tool-install-jupyter-fails\">Why <code>uv tool install jupyter<\/code> fails<a href=\"#why-uv-tool-install-jupyter-fails\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h3>\n<p>Running <code>uv tool install jupyter<\/code> fails because the package <code>jupyter<\/code> doesn't provide any commands by itself.<\/p>\n<h3 id=\"why-uv-tool-install-jupyter-core-doesn-t-work\">Why <code>uv tool install jupyter-core<\/code> doesn't work<a href=\"#why-uv-tool-install-jupyter-core-doesn-t-work\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h3>\n<p>The command <code>uv tool install jupyter-core<\/code> looks like it works because it installs the command <code>jupyter<\/code> correctly.\nHowever, if you use <code>--help<\/code> you can see that you don't have access to the subcommands you need:<\/p>\n<pre><code class=\"language-bash\">$ uv tool install jupyter-core\n...\nInstalled 3 executables: jupyter, jupyter-migrate, jupyter-troubleshoot\n$ jupyter --help\n...\nAvailable subcommands: book migrate troubleshoot<\/code><\/pre>\n<p>That's because the subcommands <code>notebook<\/code> and <code>lab<\/code> are from the package <code>jupyter<\/code>.\nThe solution?\nInstall <code>jupyter-core<\/code> <em>with<\/em> the additional dependency <code>jupyter<\/code>, which is what the command <code>uv tool install --with jupyter jupyter-core<\/code> does.<\/p>\n<h2 id=\"other-usages-of-jupyter\">Other usages of Jupyter<a href=\"#other-usages-of-jupyter\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>The uv documentation has a <a href=\"https:\/\/docs.astral.sh\/uv\/guides\/integration\/jupyter\/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" class=\"external-link no-image\">page dedicated exclusively to the usage of uv with Jupyter<\/a>, so check it out for other use cases of the uv and Jupyter combo!<\/p>","summary":"Today I learned how to install jupyter properly while using uv to manage tools.","date_modified":"2026-03-03T18:05:58+01:00","tags":["python","programming","uv","productivity"],"image":"\/user\/pages\/02.blog\/04.til\/140.install-jupyter-with-uv\/thumbnail.webp"},{"title":"TIL #139 \u2013 Multiline input in the REPL","date_published":"2026-03-02T15:15:00+01:00","id":"https:\/\/mathspp.com\/blog\/til\/multiline-input-in-the-repl","url":"https:\/\/mathspp.com\/blog\/til\/multiline-input-in-the-repl","content_html":"<p>Today I learned how to do multiline input in the REPL using an uncommon combination of arguments for the built-in <code>open<\/code>.<\/p>\n\n<p>A while ago <a href=\"\/blog\/til\/020\">I learned I could use <code>open(0)<\/code> to open standard input<\/a>.\nThis unlocks a neat trick that allows you to do multiline input in the REPL:<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; msg = open(0).read()\nHello,\nworld!\n^D\n&gt;&gt;&gt; msg\n'Hello,\\nworld!\\n'<\/code><\/pre>\n<p>The cryptic <code>^D<\/code> is <kbd>Ctrl<\/kbd>+<kbd>D<\/kbd>, which means EOF on Unix systems.\nIf you're on Windows, use <kbd>Ctrl<\/kbd>+<kbd>Z<\/kbd>.<\/p>\n<p>The problem is that if you try to use <code>open(0).read()<\/code> again to read more multiline input, you get an exception:<\/p>\n<pre><code class=\"language-py\">OSError: [Errno 9] Bad file descriptor<\/code><\/pre>\n<p>That's because, when you finished reading the first time around, Python closed the file descriptor <code>0<\/code>, so you can no longer use it.<\/p>\n<p>The fix is to set <code>closefd=False<\/code> when you use the built-in <code>open<\/code>.\nWith the parameter <code>closefd<\/code> set to <code>False<\/code>, the underlying file descriptor isn't closed and you can reuse it:<\/p>\n<pre><code class=\"language-pycon\">&gt;&gt;&gt; msg1 = open(0, closefd=False).read()\nHello,\nworld!\n^D\n&gt;&gt;&gt; msg1\n'Hello,\\nworld!\\n'\n\n&gt;&gt;&gt; msg2 = open(0, closefd=False).read()\nGoodbye,\nworld!\n^D\n&gt;&gt;&gt; msg2\n'Goodbye,\\nworld!\\n'<\/code><\/pre>\n<p>By using <code>open(0, closefd=False)<\/code>, you can read multiline input in the REPL <em>repeatedly<\/em>.<\/p>","summary":"Today I learned how to do multiline input in the REPL using an uncommon combination of arguments for the built-in open.","date_modified":"2026-03-02T16:21:46+01:00","tags":["repl","programming","python"],"image":"\/user\/pages\/02.blog\/04.til\/139.multiline-input-in-the-repl\/thumbnail.webp"},{"title":"TIL #138 \u2013 Custom directives in Jupyter Book","date_published":"2026-01-02T22:22:00+01:00","id":"https:\/\/mathspp.com\/blog\/til\/custom-directives-in-jupyter-book","url":"https:\/\/mathspp.com\/blog\/til\/custom-directives-in-jupyter-book","content_html":"<p>Today I learned how to create and register a simple Sphinx extension to use as a custom directive in a Jupyter Book project.<\/p>\n\n<p>I wanted to create a custom directive that I could use in a <a href=\"https:\/\/jupyterbook.org\/v1\/start\/overview.html\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" class=\"external-link no-image\">Jupyter Book<\/a> project that would look like this:<\/p>\n<pre><code>Some prose goes here.\n\n```{mypy} snippet.py\n```<\/code><\/pre>\n<p>Then, the directive <code>{mypy}<\/code> would run mypy against the file <code>snippet.py<\/code> and include the mypy output in the book.<\/p>\n<p>With the help of ChatGPT I was able to quickly whip up a Sphinx extension that defines this directive, including the ability to infer the location of my Python snippets based on the concrete structure I have for this project I'm working on: a file <code>snippet.py<\/code> mentioned in a chapter called <code>xx.my-chapter.md<\/code> can be found in <code>snippets\/my-chapter\/snippet.py<\/code>, where <code>snippets<\/code> is at the root of the project.<\/p>\n<p>After a bit of back and forth and some manual tweaks, this is the directive I ended up with:<\/p>\n<details><summary><code>_ext\/mypy_directive.py<\/code><\/summary><pre><code>\"\"\"\nCreates a Sphinx directive {mypy} that runs mypy on the given file and includes the output.\nWhen used as ```{mypy} script.py in a file called `xx.some-chapter.md`, this directive\nwill try to find the file in `snippets\/some-chapter\/script.py`.\nIf the file argument contains slashes, it is interpreted as an absolute path.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\nfrom pathlib import Path\nfrom typing import Any, List\n\nfrom docutils import nodes\nfrom sphinx.util.docutils import SphinxDirective\n\nclass MypyDirective(SphinxDirective):\n    \"\"\"\n    Usage (MyST):\n        ```{mypy} path\/to\/file.py\n        :flags: --strict --show-error-codes\n        ```\n    \"\"\"\n\n    required_arguments = 1  # the file path\n    optional_arguments = 0\n    has_content = False\n\n    option_spec = {\n        \"flags\": lambda s: s,  # pass extra mypy CLI flags as a single string\n    }\n\n    def run(self) -&amp;gt; List[nodes.Node]:\n        script_arg = self.arguments[0]\n        env = self.env\n\n        # Get current document filename (e.g. \"given-chapter\")\n        # env.docname is like \"chapters\/given-chapter\" (no extension)\n        _, _, chapter_name = Path(env.docname).name.partition(\".\")\n\n        # If the user passed just \"script.py\", infer snippets\/&lt;chapter&gt;\/&lt;script.py&gt;.\n        # If they passed a path with a slash, treat it as explicit.\n        if \"\/\" not in script_arg and \"\\\\\" not in script_arg:\n            inferred_rel = str(Path(\"snippets\") \/ chapter_name \/ script_arg)\n        else:\n            inferred_rel = script_arg\n\n        # Sphinx helper: resolve filenames relative to doc, and track dependencies\n        _, abs_path_str = env.relfn2path(inferred_rel)\n        abs_path = Path(abs_path_str)\n\n        # Ensure rebuilds happen when the file changes\n        env.note_dependency(str(abs_path))\n\n        if not abs_path.exists():\n            msg = f\"[mypy] File not found: {abs_path}\"\n            return [nodes.literal_block(text=msg)]\n\n        flags = self.options.get(\"flags\", \"\").strip()\n        cmd: list[str] = [\"mypy\", str(abs_path)]\n        if flags:\n            cmd.extend(flags.split())\n\n        proc = subprocess.run(\n            cmd,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            text=True,\n            cwd=abs_path.parent,\n        )\n\n        output = proc.stdout.rstrip()\n        if not output:\n            output = \"[mypy] (no output)\"\n\n        output = f\"$ mypy {abs_path.name}\\n\" + output\n\n        # Render as a literal block (monospace). &ldquo;language&rdquo; here is just for CSS\/classes.\n        block = nodes.literal_block(output, output)\n        block[\"language\"] = \"text\"\n        return [block]\n\ndef setup(app: Any) -&amp;gt; dict[str, Any]:\n    app.add_directive(\"mypy\", MypyDirective)\n    return {\"version\": \"0.1\", \"parallel_read_safe\": True, \"parallel_write_safe\": True}<\/code><\/pre>\n<p>&lt;\/script.py&gt;<\/p>\n<\/details><p>To be able to use it, I had to tweak the book configuration to tell it where to find my extension:<\/p>\n<pre><code class=\"language-yaml\">sphinx:\n  ...\n  local_extensions:\n    mypy_directive: _ext\n  extra_extensions:\n    - mypy_directive<\/code><\/pre>\n<p>This assumes the code <code>mypy_directive.py<\/code> lives inside <code>_ext<\/code> in...<\/p>","summary":"Today I learned how to create and register a simple Sphinx extension to use as a custom directive in a Jupyter Book project.","date_modified":"2026-07-06T16:16:52+02:00","tags":["productivity","llms"],"image":"\/user\/pages\/02.blog\/04.til\/138.custom-directives-in-jupyter-book\/thumbnail.webp"},{"title":"TIL #137 \u2013 Inline SVGs in Jupyter notebooks","date_published":"2025-11-23T23:34:00+01:00","id":"https:\/\/mathspp.com\/blog\/til\/inline-svgs-in-jupyter-notebooks","url":"https:\/\/mathspp.com\/blog\/til\/inline-svgs-in-jupyter-notebooks","content_html":"<p>Today I learned how to inline SVGs in Jupyter notebooks in two simple steps.<\/p>\n\n<p>Today I learned how to inline SVGs in Jupyter notebooks in two simple steps:<\/p>\n<ol>\n<li>URL-encode the SVG markup. If you have an SVG file, open it and copy the contents of the file starting from the <code>&lt;svg&gt;<\/code> tag all the way up to the closing <code>&lt;\/svg&gt;<\/code> and encode it so it's safe to use in a URL. You can use <a href=\"https:\/\/tools.mathspp.com\/url-encode\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" class=\"external-link no-image\">this URL encoder tool I created<\/a>.<\/li>\n<li>Add an image using Markdown syntax with <code>![ALT text](data:image\/svg+xml,&lt;URL-encoded string here&gt;)<\/code>.<\/li>\n<\/ol>\n<p>For any non-trivial SVG the URL-encoded string will look huge and nasty, as the image below shows:<\/p>\n<figure class=\"image-caption\"><img title=\"The URL-encoded SVG.\" alt=\"Screenshot of a Jupyter notebok with a Markdown cell being edited. The markup in the cell starts with \u201c![](data:image\/svg+xml,\u201d and is followed by a very long string of weird-looking characters with lots of percent signs.\" src=\"\/user\/pages\/02.blog\/04.til\/137.inline-svgs-in-jupyter-notebooks\/_markup.webp\"><figcaption class=\"\">The URL-encoded SVG.<\/figcaption><\/figure>\n<p>But when I \u201cexecute\u201d the cell to render the Markdown, the SVG displays neatly:<\/p>\n<figure class=\"image-caption\"><img title=\"The rendered SVG.\" alt=\"Screenshot of an SVG showing 8 diagrams with white and black squares arranged in the same shape but with different colouring schemes.\" src=\"\/user\/pages\/02.blog\/04.til\/137.inline-svgs-in-jupyter-notebooks\/_svg.webp\"><figcaption class=\"\">The rendered SVG.<\/figcaption><\/figure>\n<p>This was an interesting endeavour because I thought I could just paste the SVG markup in the notebook cell and it would be rendered; I was under the impression that you could write arbitrary HTML in those cells.\nI was either wrong or I did it in the wrong way!<\/p>","summary":"Today I learned how to inline SVGs in Jupyter notebooks in two simple steps.","date_modified":"2025-11-24T01:18:24+01:00","tags":["productivity","programming"],"image":"\/user\/pages\/02.blog\/04.til\/137.inline-svgs-in-jupyter-notebooks\/thumbnail.webp"},{"title":"TIL #136 \u2013 Publish an EPUB book with Jupyter Book","date_published":"2025-11-17T11:01:00+01:00","id":"https:\/\/mathspp.com\/blog\/til\/publish-an-epub-book-with-jupyter-book","url":"https:\/\/mathspp.com\/blog\/til\/publish-an-epub-book-with-jupyter-book","content_html":"<p>Today I learned how to set the configurations of my Jupyter Book to build my book in the EPUB format.<\/p>\n\n<h1 id=\"publish-an-epub-book-with-jupyter-book\">Publish an EPUB book with Jupyter Book<a href=\"#publish-an-epub-book-with-jupyter-book\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h1>\n<p>I've been redoing my book <a href=\"\/books\/pydonts\">Pydon'ts \u2013 Write elegant Python code<\/a> in <a href=\"https:\/\/jupyterbook.org\" target=\"_blank\" rel=\"nofollow noopener noreferrer\" class=\"external-link no-image\">Jupyter Book<\/a> (v1) because the PDFs have great typesetting defaults and look much better than what I currently have with pandoc plus a couple of custom filters.<\/p>\n<p>I was trying to also get Jupyter Book to build my book in EPUB format but was struggling a bit with it because I was getting a couple of weird warnings when I ran the build command:<\/p>\n<pre><code class=\"language-bash\">bash % jb build --all --builder=custom --custom-builder=epub .<\/code><\/pre>\n<p>This created an EPUB with the name <code>Projectnamenotset.epub<\/code> and the book title was <code>Projectnamenotset<\/code>, so I knew something was off.<\/p>\n<p>The configuration file had the correct metadata:<\/p>\n<pre><code class=\"language-yaml\"># _config.yml\ntitle: Pydon'ts \u2013 Write elegant Python code\nauthor: Rodrigo Gir\u00e3o Serr\u00e3o\n# ...<\/code><\/pre>\n<div class=\"notices yellow\">\n<p>Strictly speaking, I was only getting warnings so the build process was working fine.\nBut I wanted to run the flag <code>-W<\/code>, which turns warnings into errors, so I was hitting a couple of roadblocks on top of the weird project name.<\/p>\n<\/div>\n<h2 id=\"epub3-requires-a-version\">EPUB3 requires a version<a href=\"#epub3-requires-a-version\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>I was getting a warning saying that the format EPUB3 required a non-empty version, which just meant I had to specify a version in the Sphinx config:<\/p>\n<pre><code class=\"language-yaml\">sphinx:\n  config:\n    version: \"2025.11.17\"<\/code><\/pre>\n<h2 id=\"setting-the-epub-file-name\">Setting the EPUB file name<a href=\"#setting-the-epub-file-name\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>To set the EPUB file name to something other than <code>Projectnamenotset.epub<\/code> I had to set the option <code>epub_basename<\/code> in the Sphinx config:<\/p>\n<pre><code class=\"language-yaml\"># _config.yml\nsphinx:\n  config:\n    # ...\n    epub_basename: \"pydonts\"<\/code><\/pre>\n<h2 id=\"setting-the-epub-title\">Setting the EPUB title<a href=\"#setting-the-epub-title\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>Although I had the <code>title<\/code> metadata set, I had to set it again in the Sphinx config so it would show as the EPUB title:<\/p>\n<pre><code class=\"language-yaml\">sphinx:\n  config:\n    # ...\n    epub_title: \"Pydon'ts \u2013 Write elegant Python code\"<\/code><\/pre>\n<h2 id=\"unknown-mimetype-for-index-html\">Unknown mimetype for <code>index.html<\/code><a href=\"#unknown-mimetype-for-index-html\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>Since my root file is not called <code>index<\/code>, I was also getting a warning saying <code>sphinx.errors.SphinxWarning: unknown mimetype for index.html<\/code>.<\/p>\n<p>Long story short, an extension wants me to have the file <code>index.html<\/code> so that I can always navigate to the root URL and see something, and then the file <code>index.html<\/code> just redirects to my custom root, which is <code>foreword<\/code> in this case:<\/p>\n<pre><code class=\"language-yaml\"># _toc.yml\nformat: jb-book\nroot: foreword  # &lt;--\nparts:\n  - caption: Introduction\n    chapters:\n    - file: pydonts\/pydont-disrespect-the-zen-of-python.md\n    # ...<\/code><\/pre>\n<p>So, I had to tell the EPUB builder to ignore the file <code>index.html<\/code>, that was being built but shouldn't be used when building the final EPUB:<\/p>\n<pre><code class=\"language-yaml\">sphinx:\n  config:\n    epub_exclude_files:\n      - \"index.html\"<\/code><\/pre>\n<h2 id=\"final-configuration\">Final configuration<a href=\"#final-configuration\" class=\"toc-anchor after\" data-anchor-icon=\"#\" aria-label=\"Anchor\"><\/a><\/h2>\n<p>Here's the final set of configurations I use to build the EPUB:<\/p>\n<pre><code class=\"language-yaml\">sphinx:\n  config:\n    epub_exclude_files:\n      - \"index.html\"\n    epub_basename: \"pydonts\"\n    epub_title: \"Pydon'ts \u2013 Write elegant Python code\"\n    version: \"2025.11.17\"\n    language: en<\/code><\/pre>\n<p>I build with this command:<\/p>\n<pre><code class=\"language-bash\">bash % jb build --all --builder=custom --custom-builder=epub -W .<\/code><\/pre>","summary":"Today I learned how to set the configurations of my Jupyter Book to build my book in the EPUB format.","date_modified":"2025-11-17T12:20:19+01:00","tags":["productivity"],"image":"\/user\/pages\/02.blog\/04.til\/136.publish-an-epub-book-with-jupyter-book\/thumbnail.webp"}]}
