<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>python &amp;mdash; Nat Knight</title>
    <link>http://natknight.xyz/tag:python</link>
    <description>Reflections, diversions, and opinions from a progressive ex-physicist programmer dad with a sore back.</description>
    <pubDate>Sat, 23 May 2026 13:44:25 -0700</pubDate>
    <item>
      <title>Lazy-loading data modules in Python with one magic function</title>
      <link>http://natknight.xyz/lazy-loading-data-modules-in-python-with-one-magic-function</link>
      <description>&lt;![CDATA[#python #data&#xA;&#xA;Greg Wilson was soliciting strategies for lazy-loading datasets in Python modules. There are, of course, many ways to do this, but I didn&#39;t see this one being discussed.&#xA;&#xA;Since Python 3.7 (released in 2017) you can define a module-level getattr function that gets used for name resolution (see PEP 562). You can use this to call a data-loading function the first time each module-level property is accessed. This implementation uses only functions, dicts, and the one magic &#34;dunder&#34; function, which might be more approachable than programming with classes (depending on your audience).&#xA;&#xA;!--more--&#xA;&#xA;A sketch of an implementation:&#xA;&#xA;Cache loaded datasets&#xA;datasetcache = {}&#xA;&#xA;Dictionary mapping dataset names to loader functions&#xA;datasetloaders = {&#xA;  # maps names to argument-less functions that load datasets&#xA;}&#xA;&#xA;def getattr(name):&#xA;    &#34;&#34;&#34;PEP 562 module-level getattr function for lazy loading&#34;&#34;&#34;&#xA;    # Check if we should load a dataset for this attribute&#xA;    if name in datasetloaders:&#xA;        # If not already cached, load and cache it&#xA;        if name not in datasetcache:&#xA;            datasetcachename] = datasetloaders[name&#xA;        return datasetcache[name]&#xA;&#xA;    # If not a dataset, raise AttributeError&#xA;    raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)&#xA;&#xA;For example, this module has attributes foo and bar which are only calculated when they&#39;re first referenced:&#xA;&#xA;foo.py&#xA;&#xA;Cache loaded datasets&#xA;datasetcache = {}&#xA;&#xA;def loaddata(name):&#xA;    if name == &#34;foo&#34;:&#xA;        print(&#34;loading foo&#34;)&#xA;        return 1&#xA;    elif name == &#34;bar&#34;:&#xA;        print(&#34;loading bar&#34;)&#xA;        return 2&#xA;    else:&#xA;        # This should be unreachable, but just in case...&#xA;        raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)&#xA;&#xA;Dictionary mapping dataset names to loader functions&#xA;datasetloaders = {&#xA;    &#34;foo&#34;: lambda: loaddata(&#34;foo&#34;),&#xA;    &#34;bar&#34;: lambda: loaddata(&#34;bar&#34;),&#xA;}&#xA;&#xA;def getattr(name):&#xA;    &#34;&#34;&#34;PEP 562 module-level getattr function for lazy loading&#34;&#34;&#34;&#xA;    # Check if we should load a dataset for this attribute&#xA;    if name in datasetloaders:&#xA;        # If not already cached, load and cache it&#xA;        if name not in datasetcache:&#xA;            datasetcachename] = datasetloaders[name&#xA;        return dataset_cache[name]&#xA;&#xA;    # If not a dataset, raise AttributeError&#xA;    raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)&#xA;&#xA;In use, this looks like:&#xA;&#xA;      import foo&#xA;      foo.foo&#xA;loading foo&#xA;1&#xA;      foo.foo&#xA;1&#xA;      # NOTE: didn&#39;t load foo a second time&#xA;      foo.bar&#xA;loading bar&#xA;2&#xA;      foo.quuz&#xA;Traceback (most recent call last):&#xA;  File &#34;python-input-4&#34;, line 1, in module&#xA;    foo.quuz&#xA;  File &#34;/Users/nknight/tmp/foo.py&#34;, line 34, in getattr&#xA;    raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)&#xA;AttributeError: Module has no attribute &#39;quuz&#39;&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a> <a href="http://natknight.xyz/tag:data" class="hashtag"><span>#</span><span class="p-category">data</span></a></p>

<p>Greg Wilson was <a href="https://third-bit.com/2025/04/21/lazy-loading-data-package/">soliciting</a> strategies for lazy-loading datasets in Python modules. There are, of course, many ways to do this, but I didn&#39;t see this one being discussed.</p>

<p>Since Python 3.7 (released in 2017) you can define a module-level <code>__getattr__</code> function that gets used for name resolution (see <a href="https://peps.python.org/pep-0562/">PEP 562</a>). You can use this to call a data-loading function the first time each module-level property is accessed. This implementation uses only functions, dicts, and the one magic “dunder” function, which might be more approachable than programming with classes (depending on your audience).</p>



<p>A sketch of an implementation:</p>

<pre><code class="language-python"># Cache loaded datasets
_dataset_cache = {}

# Dictionary mapping dataset names to loader functions
_dataset_loaders = {
  # maps names to argument-less functions that load datasets
}


def __getattr__(name):
    &#34;&#34;&#34;PEP 562 module-level __getattr__ function for lazy loading&#34;&#34;&#34;
    # Check if we should load a dataset for this attribute
    if name in _dataset_loaders:
        # If not already cached, load and cache it
        if name not in _dataset_cache:
            _dataset_cache[name] = _dataset_loaders[name]()
        return _dataset_cache[name]

    # If not a dataset, raise AttributeError
    raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)
</code></pre>

<p>For example, this module has attributes <code>foo</code> and <code>bar</code> which are only calculated when they&#39;re first referenced:</p>

<pre><code class="language-python"># foo.py

# Cache loaded datasets
_dataset_cache = {}


def load_data(name):
    if name == &#34;foo&#34;:
        print(&#34;loading foo&#34;)
        return 1
    elif name == &#34;bar&#34;:
        print(&#34;loading bar&#34;)
        return 2
    else:
        # This should be unreachable, but just in case...
        raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)


# Dictionary mapping dataset names to loader functions
_dataset_loaders = {
    &#34;foo&#34;: lambda: load_data(&#34;foo&#34;),
    &#34;bar&#34;: lambda: load_data(&#34;bar&#34;),
}


def __getattr__(name):
    &#34;&#34;&#34;PEP 562 module-level __getattr__ function for lazy loading&#34;&#34;&#34;
    # Check if we should load a dataset for this attribute
    if name in _dataset_loaders:
        # If not already cached, load and cache it
        if name not in _dataset_cache:
            _dataset_cache[name] = _dataset_loaders[name]()
        return _dataset_cache[name]

    # If not a dataset, raise AttributeError
    raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)
</code></pre>

<p>In use, this looks like:</p>

<pre><code>&gt;&gt;&gt; import foo
&gt;&gt;&gt; foo.foo
loading foo
1
&gt;&gt;&gt; foo.foo
1
&gt;&gt;&gt; # NOTE: didn&#39;t load foo a second time
&gt;&gt;&gt; foo.bar
loading bar
2
&gt;&gt;&gt; foo.quuz
Traceback (most recent call last):
  File &#34;&lt;python-input-4&gt;&#34;, line 1, in &lt;module&gt;
    foo.quuz
  File &#34;/Users/nknight/tmp/foo.py&#34;, line 34, in __getattr__
    raise AttributeError(f&#34;Module has no attribute &#39;{name}&#39;&#34;)
AttributeError: Module has no attribute &#39;quuz&#39;
</code></pre>
]]></content:encoded>
      <guid>http://natknight.xyz/lazy-loading-data-modules-in-python-with-one-magic-function</guid>
      <pubDate>Tue, 22 Apr 2025 17:49:33 +0000</pubDate>
    </item>
    <item>
      <title>TIL: uv --script</title>
      <link>http://natknight.xyz/til-uv-script</link>
      <description>&lt;![CDATA[#uv #python #til #scripting&#xA;&#xA;I&#39;ve known for a while that uv can run Python scripts that declare inline dependencies.&#xA;&#xA;I learned today that you can also use uv to manage those dependencies.&#xA;&#xA;!--more--&#xA;&#xA;If you have a file foo.py:&#xA;&#xA;!/usr/bin/env -S uv run&#xA;&#xA;print(&#34;your code here&#34;)&#xA;&#xA;you can run uv add --script foo.py numpy and it becomes:&#xA;&#xA;!/usr/bin/env -S uv run&#xA;/// script&#xA;requires-python = &#34;  =3.12&#34;&#xA;dependencies = [&#xA;&#34;numpy&#34;,&#xA;]&#xA;///&#xA;&#xA;print(&#34;your code here&#34;)&#xA;&#xA;This is obviously nice if, like me, you have trouble remembering the exact syntax for declaring the inline dependencies, but it also means uv does things like dependency resolution so you don&#39;t have to manage your script&#39;s dependencies by hand.&#xA;&#xA;You can add --script to:&#xA;&#xA;uv add and uv remove to add and remove dependencies&#xA;uv lock and uv sync to create and synchronize with the script&#39;s lock file&#xA;uv tree to print the script&#39;s transitive dependencies&#xA;uv export to export the script&#39;s dependencies to another format, such as requirements.txt&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:uv" class="hashtag"><span>#</span><span class="p-category">uv</span></a> <a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a> <a href="http://natknight.xyz/tag:til" class="hashtag"><span>#</span><span class="p-category">til</span></a> <a href="http://natknight.xyz/tag:scripting" class="hashtag"><span>#</span><span class="p-category">scripting</span></a></p>

<p>I&#39;ve known for a while that <code>uv</code> can <a href="https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies">run Python scripts</a> that declare <a href="https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata">inline dependencies</a>.</p>

<p>I learned today that you can also use <code>uv</code> to <em>manage</em> those dependencies.</p>



<p>If you have a file <code>foo.py</code>:</p>

<pre><code class="language-python">#!/usr/bin/env -S uv run

print(&#34;your code here&#34;)
</code></pre>

<p>you can run <code>uv add --script foo.py numpy</code> and it becomes:</p>

<pre><code class="language-python">#!/usr/bin/env -S uv run
# /// script
# requires-python = &#34;&gt;=3.12&#34;
# dependencies = [
#     &#34;numpy&#34;,
# ]
# ///

print(&#34;your code here&#34;)
</code></pre>

<p>This is obviously nice if, like me, you have trouble remembering the exact syntax for declaring the inline dependencies, but it also means <code>uv</code> does things like dependency resolution so you don&#39;t have to manage your script&#39;s dependencies by hand.</p>

<p>You can add <code>--script</code> to:</p>
<ul><li><code>uv add</code> and <code>uv remove</code> to add and remove dependencies</li>
<li><code>uv lock</code> and <code>uv sync</code> to create and synchronize with the script&#39;s lock file</li>
<li><code>uv tree</code> to print the script&#39;s transitive dependencies</li>
<li><code>uv export</code> to export the script&#39;s dependencies to another format, such as <code>requirements.txt</code></li></ul>
]]></content:encoded>
      <guid>http://natknight.xyz/til-uv-script</guid>
      <pubDate>Tue, 04 Mar 2025 17:59:28 +0000</pubDate>
    </item>
    <item>
      <title>TIL: Time-stamp based dependency constraints with uv</title>
      <link>http://natknight.xyz/til-time-stamp-based-dependency-constraints-with-uv</link>
      <description>&lt;![CDATA[#til #python #uv&#xA;&#xA;(via epistasis on HN)&#xA;&#xA;Semantic versioning is difficult. Not everyone has the same idea of what &#34;breaking change&#34; means, and depending on your language and tooling &#34;breaking&#34; changes can sneak in despite your best efforts.&#xA;&#xA;One possible mitigation is to record when you resolved your dependencies and ignore anything published after that date. It&#39;s crude, and relies on package dependencies not monkeying with stuff that&#39;s already been published, but it&#39;s easy to understand and you can do it with uv.&#xA;&#xA;It&#39;s documented here.&#xA;&#xA;!---more---&#xA;&#xA;Put a date in your pyproject.toml file and subsequent invocations of uv will ignore anything published after the cutoff:&#xA;&#xA;[tool.uv]&#xA;exclude-newer = &#34;2023-10-16T00:00:00Z&#34;&#xA;&#xA;You can also use it in the &#34;inline metadata&#34; format for scripting! From the uv docs:&#xA;&#xA;/// script&#xA;dependencies = [&#xA;&#34;requests&#34;,&#xA;]&#xA;[tool.uv]&#xA;exclude-newer = &#34;2023-10-16T00:00:00Z&#34;&#xA;///&#xA;&#xA;import requests&#xA;&#xA;print(requests.version)&#xA;&#xA;This use case seems particularly valuable: if I write a quick script I probably care more about it not breaking than I care about it getting updated dependencies. It&#39;s nice that uv offers a way to keep code running rather than forcing me to update it or throw it out.]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:til" class="hashtag"><span>#</span><span class="p-category">til</span></a> <a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a> <a href="http://natknight.xyz/tag:uv" class="hashtag"><span>#</span><span class="p-category">uv</span></a></p>

<p>(via <a href="https://news.ycombinator.com/item?id=43097209">epistasis on HN</a>)</p>

<p>Semantic versioning is difficult. Not everyone has the same idea of what “breaking change” means, and depending on your language and tooling “breaking” changes can sneak in despite your best efforts.</p>

<p>One possible mitigation is to record <em>when</em> you resolved your dependencies and ignore anything published after that date. It&#39;s crude, and relies on package dependencies not monkeying with stuff that&#39;s already been published, but it&#39;s easy to understand and you can do it with <a href="https://docs.astral.sh/uv/"><code>uv</code></a>.</p>

<p>It&#39;s documented <a href="https://docs.astral.sh/uv/guides/scripts/#improving-reproducibility">here</a>.</p>



<p>Put a date in your <code>pyproject.toml</code> file and subsequent invocations of <code>uv</code> will ignore anything published after the cutoff:</p>

<pre><code class="language-toml">[tool.uv]
exclude-newer = &#34;2023-10-16T00:00:00Z&#34;
</code></pre>

<p>You can also use it in the <a href="https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies">“inline metadata” format for scripting</a>! From the <code>uv</code> docs:</p>

<pre><code class="language-python"># /// script
# dependencies = [
#   &#34;requests&#34;,
# ]
# [tool.uv]
# exclude-newer = &#34;2023-10-16T00:00:00Z&#34;
# ///

import requests

print(requests.__version__)
</code></pre>

<p>This use case seems particularly valuable: if I write a quick script I probably care more about it not breaking than I care about it getting updated dependencies. It&#39;s nice that <code>uv</code> offers a way to keep code running rather than forcing me to update it or throw it out.</p>
]]></content:encoded>
      <guid>http://natknight.xyz/til-time-stamp-based-dependency-constraints-with-uv</guid>
      <pubDate>Thu, 20 Feb 2025 23:56:36 +0000</pubDate>
    </item>
    <item>
      <title>Release: llm-questioncache</title>
      <link>http://natknight.xyz/release-llm-questioncache</link>
      <description>&lt;![CDATA[#python #llm #embeddings #release #simonwillison&#xA;&#xA;I just released version 0.1 of a plugin for Simon Willison&#39;s llm called llm-questioncache. It lets you send questions to your default LLM with a system prompt that elicits short, to-the-point answers. It also maintains a cache of answers locally so that you only have to hit the LLM once for each bit of esoteric knowledge.&#xA;&#xA;!--more--&#xA;&#xA;It uses embeddings of each question to find similar questions so that (for example) if you ask&#xA;&#xA;  How do you compare two branches in git&#xA;&#xA;and&#xA;&#xA;  How to compare different branches in git&#xA;&#xA;you&#39;ll get the same answer.&#xA;&#xA;If you&#39;ve already got LLM installed you can try it out with &#xA;&#xA;llm install llm-questioncache&#xA;&#xA;Here&#39;s the PyPI package:&#xA;https://pypi.org/project/llm-questioncache/&#xA;&#xA;And here&#39;s the source code:&#xA;https://github.com/nathanielknight/llm-questioncache&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a> <a href="http://natknight.xyz/tag:llm" class="hashtag"><span>#</span><span class="p-category">llm</span></a> <a href="http://natknight.xyz/tag:embeddings" class="hashtag"><span>#</span><span class="p-category">embeddings</span></a> <a href="http://natknight.xyz/tag:release" class="hashtag"><span>#</span><span class="p-category">release</span></a> <a href="http://natknight.xyz/tag:simonwillison" class="hashtag"><span>#</span><span class="p-category">simonwillison</span></a></p>

<p>I just released version 0.1 of a plugin for Simon Willison&#39;s <a href="https://github.com/simonw/llm"><code>llm</code></a> called <a href="https://github.com/nathanielknight/llm-questioncache"><code>llm-questioncache</code></a>. It lets you send questions to your default LLM with a system prompt that elicits short, to-the-point answers. It also maintains a cache of answers locally so that you only have to hit the LLM once for each bit of esoteric knowledge.</p>



<p>It uses <a href="https://vickiboykis.com/what_are_embeddings/">embeddings</a> of each question to find similar questions so that (for example) if you ask</p>

<blockquote><p>How do you compare two branches in git</p></blockquote>

<p>and</p>

<blockquote><p>How to compare different branches in git</p></blockquote>

<p>you&#39;ll get the same answer.</p>

<p>If you&#39;ve already got LLM installed you can try it out with</p>

<pre><code>llm install llm-questioncache
</code></pre>

<p>Here&#39;s the PyPI package:
<a href="https://pypi.org/project/llm-questioncache/">https://pypi.org/project/llm-questioncache/</a></p>

<p>And here&#39;s the source code:
<a href="https://github.com/nathanielknight/llm-questioncache">https://github.com/nathanielknight/llm-questioncache</a></p>
]]></content:encoded>
      <guid>http://natknight.xyz/release-llm-questioncache</guid>
      <pubDate>Sun, 09 Feb 2025 05:59:09 +0000</pubDate>
    </item>
    <item>
      <title>TIL: Making a Django REST framework view do server-side rendering</title>
      <link>http://natknight.xyz/til-making-a-django-rest-framework-view-do-server-side-rendering</link>
      <description>&lt;![CDATA[#til #django #djangorestramework #python&#xA;&#xA;On a recent project I found myself needing one classic form-and-template style page in an otherwise API-driven project. I could, of course, [just do it] with a regular view function, but I had a bunch of authentication and suchlike set up for DRF APIViews.&#xA;&#xA;Turns out it&#39;s actually pretty easy to make an APIView kick it oldschool!&#xA;&#xA;!--more--&#xA;&#xA;from restframework.parsers import FormParser&#xA;from restframework.renderers import TemplateHTMLRenderer&#xA;from restframework.response import Response&#xA;from restframework.views import APIView&#xA;&#xA;class OldSchoolView(APIView):&#xA;    parserclasses = [FormParser]&#xA;    rendererclasses = [TemplateHTMLRenderer]&#xA;    template_name = &#34;path/to/a/template.html&#34;&#xA;&#xA;    def get(self, request):&#xA;        context = {}&#xA;        # do regular view stuff here&#xA;        return Response(context)&#xA;&#xA;    def post(self, request):&#xA;        formdata = request.data.dict()&#xA;        # do regular view stuff here&#xA;        return Response()  # or redirect; whatever&#39;s appropriate&#xA;&#xA;The indicated template gets rendered with the context you pass to the Response.&#xA;&#xA;I think you could write a view that simultaneously supports HTML and JSON with [content negotiation], but it ended up being more trouble that it was worth in my case.&#xA;&#xA;[just do it]: https://spookylukey.github.io/django-views-the-right-way/anything.html]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:til" class="hashtag"><span>#</span><span class="p-category">til</span></a> <a href="http://natknight.xyz/tag:django" class="hashtag"><span>#</span><span class="p-category">django</span></a> <a href="http://natknight.xyz/tag:djangorestramework" class="hashtag"><span>#</span><span class="p-category">djangorestramework</span></a> <a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a></p>

<p>On a recent project I found myself needing one classic form-and-template style page in an otherwise API-driven project. I could, of course, <a href="https://spookylukey.github.io/django-views-the-right-way/anything.html">just do it</a> with a regular view function, but I had a bunch of authentication and suchlike set up for DRF <code>APIView</code>s.</p>

<p>Turns out it&#39;s actually pretty easy to make an <code>APIView</code> kick it oldschool!</p>



<pre><code class="language-python">from rest_framework.parsers import FormParser
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
from rest_framework.views import APIView


class OldSchoolView(APIView):
    parser_classes = [FormParser]
    renderer_classes = [TemplateHTMLRenderer]
    template_name = &#34;path/to/a/template.html&#34;

    def get(self, request):
        context = {}
        # do regular view stuff here
        return Response(context)

    def post(self, request):
        formdata = request.data.dict()
        # do regular view stuff here
        return Response()  # or redirect; whatever&#39;s appropriate
</code></pre>

<p>The indicated template gets rendered with the <code>context</code> you pass to the <code>Response</code>.</p>

<p>I think you could write a view that simultaneously supports HTML and JSON with <a href="https://www.django-rest-framework.org/api-guide/content-negotiation/">content negotiation</a>, but it ended up being more trouble that it was worth in my case.</p>
]]></content:encoded>
      <guid>http://natknight.xyz/til-making-a-django-rest-framework-view-do-server-side-rendering</guid>
      <pubDate>Thu, 06 Feb 2025 08:06:18 +0000</pubDate>
    </item>
    <item>
      <title>Setting Helix up as a Python IDE</title>
      <link>http://natknight.xyz/setting-helix-up-as-a-python-ide</link>
      <description>&lt;![CDATA[#helix #python&#xA;&#xA;This article describes how to get the [Helix] text editor set up to be a half-decent Python IDE. You&#39;ll need to know a little bit about the command line, but if you&#39;re using Helix you&#39;ll probably be fine.&#xA;&#xA;!--more--&#xA;&#xA;[Helix]: https://helix-editor.com/&#xA;&#xA;Instructions&#xA;&#xA;To assess how well Helix can support a particular language with the tools you already have, you can run:&#xA;&#xA;hx --health python&#xA;&#xA;Which will show you something like:&#xA;&#xA;Configured language servers:&#xA;  ✘ pylsp: &#39;pylsp&#39; not found in $PATH&#xA;Configured debug adapter: None&#xA;Configured formatter: None&#xA;Highlight queries: ✓&#xA;Textobject queries: ✓&#xA;Indent queries: ✓&#xA;&#xA;Helix needs us to install a Python Language Server. Rather than putting tools in our global Python environment or re-installing everything for each project, we&#39;ll use Pipx] to install it in its own virtualenv ([npx] is an analogous tool). If you don&#39;t have it installed, you can probably find instructions [here.&#xA;&#xA;[Pipx]: https://pipx.pypa.io/stable/&#xA;[npx]: https://docs.npmjs.com/cli/v7/commands/npx&#xA;&#xA;With Pipx installed, first we&#39;ll install the [Python LSP Server]&#xA;&#xA;pipx install python-lsp-server&#xA;&#xA;[Python LSP Server]: https://pypi.org/project/python-lsp-server/&#xA;&#xA;Because it aims to support multiple tools for things like typechecking and formatting, pylsp uses plugins to support those features, which need to be installed as separate packages. Luckily, Pipx supports this fairly well with its inject command. For example, to add support for [Black] and [Ruff], we can run:&#xA;&#xA;pipx inject python-lsp-server python-lsp-ruff python-lsp-black&#xA;&#xA;Helix also requires a little bit of configuration in its languages file so that it can tell pylsp which plugins to use (on Mac OS and Linux, this file should be at ~/.config/helix/languages/toml):&#xA;&#xA;[language-server.pylsp.config.pylsp]&#xA;plugins.ruff.enabled = true&#xA;plugins.black.enabled = true&#xA;&#xA;With that, Helix should be a reasonably fully featured Python IDE.&#xA;&#xA;Why do we need to do all this?&#xA;&#xA;Rather than building plugins for each programming language, Helix embraces the [Language Server Protocol]. Helix doesn&#39;t know about refactoring, type inference, etc. It just knows how to speak LSP, and relies on a separate tool (a &#34;language server&#34;) to provide all those nice features.&#xA;&#xA;The downside of this approach is that the responsibility for providing a language server falls to the user. The benefit is that Helix, despite being a relatively new project, has out-of-the box integration with over 200 languages, from common ones like Python and TypeScript to more niche languages like Gleam, Ada, and Unison.&#xA;&#xA;[Language Server Protocol]: https://microsoft.github.io/language-server-protocol/&#xA;&#xA;As I&#39;ve been learning Helix I&#39;ve been dreading the point where I had to do some customization to get some of the features I&#39;m used to in an editor like VS Code. I&#39;m not very fond of configuring text editors, having lost a great many focused hours to wrangling VimScript and Emacs Lisp so I wasn&#39;t looking forward to learning how Helix handles this, but I admit: setting Helix up this way was much easier than I was afraid it might be.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:helix" class="hashtag"><span>#</span><span class="p-category">helix</span></a> <a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a></p>

<p>This article describes how to get the <a href="https://helix-editor.com/">Helix</a> text editor set up to be a half-decent Python IDE. You&#39;ll need to know a little bit about the command line, but if you&#39;re using Helix you&#39;ll probably be fine.</p>



<h2 id="instructions" id="instructions">Instructions</h2>

<p>To assess how well Helix can support a particular language with the tools you already have, you can run:</p>

<pre><code class="language-sh">hx --health python
</code></pre>

<p>Which will show you something like:</p>

<pre><code class="language-sh">Configured language servers:
  ✘ pylsp: &#39;pylsp&#39; not found in $PATH
Configured debug adapter: None
Configured formatter: None
Highlight queries: ✓
Textobject queries: ✓
Indent queries: ✓
</code></pre>

<p>Helix needs us to install a Python Language Server. Rather than putting tools in our global Python environment or re-installing everything for each project, we&#39;ll use <a href="https://pipx.pypa.io/stable/">Pipx</a> to install it in its own virtualenv (<a href="https://docs.npmjs.com/cli/v7/commands/npx">npx</a> is an analogous tool). If you don&#39;t have it installed, you can probably find instructions <a href="https://pipx.pypa.io/stable/installation/">here</a>.</p>

<p>With Pipx installed, first we&#39;ll install the <a href="https://pypi.org/project/python-lsp-server/">Python LSP Server</a></p>

<pre><code class="language-sh">pipx install python-lsp-server
</code></pre>

<p>Because it aims to support multiple tools for things like typechecking and formatting, <code>pylsp</code> uses plugins to support those features, which need to be installed as separate packages. Luckily, Pipx supports this fairly well with its <code>inject</code> command. For example, to add support for [Black] and [Ruff], we can run:</p>

<pre><code class="language-sh">pipx inject python-lsp-server python-lsp-ruff python-lsp-black
</code></pre>

<p>Helix also requires a little bit of configuration in its languages file so that it can tell <code>pylsp</code> which plugins to use (on Mac OS and Linux, this file should be at <code>~/.config/helix/languages/toml</code>):</p>

<pre><code class="language-toml">[language-server.pylsp.config.pylsp]
plugins.ruff.enabled = true
plugins.black.enabled = true
</code></pre>

<p>With that, Helix should be a reasonably fully featured Python IDE.</p>

<h2 id="why-do-we-need-to-do-all-this" id="why-do-we-need-to-do-all-this">Why do we need to do all this?</h2>

<p>Rather than building plugins for each programming language, Helix embraces the <a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a>. Helix doesn&#39;t know about refactoring, type inference, etc. It just knows how to speak LSP, and relies on a separate tool (a “language server”) to provide all those nice features.</p>

<p>The downside of this approach is that the responsibility for providing a language server falls to the user. The benefit is that Helix, despite being a relatively new project, has out-of-the box integration with over 200 languages, from common ones like Python and TypeScript to more niche languages like Gleam, Ada, and Unison.</p>

<p>As I&#39;ve been learning Helix I&#39;ve been dreading the point where I had to do some customization to get some of the features I&#39;m used to in an editor like VS Code. I&#39;m not very fond of configuring text editors, having lost a great many focused hours to wrangling VimScript and Emacs Lisp so I wasn&#39;t looking forward to learning how Helix handles this, but I admit: setting Helix up this way was much easier than I was afraid it might be.</p>
]]></content:encoded>
      <guid>http://natknight.xyz/setting-helix-up-as-a-python-ide</guid>
      <pubDate>Wed, 17 Apr 2024 07:00:00 +0000</pubDate>
    </item>
    <item>
      <title>What&#39;s in a virtualenv?</title>
      <link>http://natknight.xyz/whats-in-a-virtualenv</link>
      <description>&lt;![CDATA[#python #virtualenv&#xA;&#xA;A useful question for understanding software tools is to peel back a layer of abstraction and ask what the thing underneath  is. For example:&#xA;&#xA;On a computer, text is a sequence of numbers (and a system to interpret that as letters).&#xA;An HTTP request is a blob of text with a particular format.&#xA;An interpreter (like the Python or Ruby interpreters) is a program, whose function is to execute other programs.&#xA;&#xA;Knowing this sort of thing is useful when the abstractions break (e.g. when you open a text file with the wrong encoding) or when thinking about the fundamental contours of a system&#39;s possibility space. For example, if you know that programs can be configured with environment variables and CLI flags, knowing that the Python interpreter is a program means you know where to start looking if you need to configure it.&#xA;&#xA;While there are many, many articles on the internet explaining how and why to use them, there&#39;s less information about what a Python virtual environment (or &#34;virtualenv&#34;) is. Luckily for us, it ends up not being very difficult to investigate.&#xA;&#xA;!--more--&#xA;&#xA;Looking inside a virtualenv&#xA;&#xA;Creating a virtualenv is the work of a moment. If you have Python installed, you can get one of your very own like so:&#xA;&#xA;  python -m venv ./venv&#xA;&#xA;This creates a directory called venv. Let&#39;s see what&#39;s in it!&#xA;&#xA;  cd venv&#xA;  ls&#xA;...&#xA;&#xA;Listing the directory&#39;s contents reveals three directories and a file:&#xA;&#xA;Include/, which appears to be empty,&#xA;Lib/, which contains a directory called site-packages,&#xA;Scripts/, which contains a bunch of executables, including the Python interpreter and the activate/deactivate scripts for this virtualenv, and&#xA;pyvenv.cfg, a config file with the Python version the path to my system Python interpreter.&#xA;&#xA;(Note: This was run on a Windows machine; you&#39;ll get different-but-analogous results if you run it on Linux or MacOS).&#xA;&#xA;I know from prior experience that site-packages is where Python packages get installed, that &#34;include&#34; is probably something to do with compiled extensions. It also makes sense that there would be a central place to put scripts, so that the virtualenv&#39;s activation script can add it to your shell&#39;s PATH. So a virtualenv is a directory full of things for Python to import or execute. But how does it work?&#xA;&#xA;Understanding virtualenvs&#xA;&#xA;The documentation of virtualenvironments is, again, very helpful if you want to understand how to use a virtualenvironment, but not particularly illuminating on the topic of its inner workings.&#xA;&#xA;Instead the answer is in the documentation for the site package: it turns out that when the Python interpreter starts up it looks for a pyvenv.cfg file one directory above itself. If it finds one, it knows its in a virtualenv and configures itself accordingly.&#xA;&#xA;Now we know a few things about virtualenvs:&#xA;&#xA;You can use the Python interpreter at venv/scripts/python without activating the virtualenv.&#xA;&#xA;If you need to find the source code of a dependency (for debugging, say) you can find it in venv/Lib/site-packages/.&#xA;&#xA;You can ruin your virtualenv by messing with the pyvenv.cfg file (or, possibly, un-mess it to fix a ruined virtualenv).&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a> <a href="http://natknight.xyz/tag:virtualenv" class="hashtag"><span>#</span><span class="p-category">virtualenv</span></a></p>

<p>A useful question for understanding software tools is to peel back a layer of abstraction and ask what the thing underneath  <em>is</em>. For example:</p>
<ul><li>On a computer, text <em>is</em> a sequence of numbers (and a <a href="https://en.wikipedia.org/wiki/UTF-8">system to interpret that as letters</a>).</li>
<li>An HTTP request <em>is</em> a blob of text with a <a href="https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Example_session">particular format</a>.</li>
<li>An interpreter (like the Python or Ruby interpreters) <em>is</em> a program, whose function is to execute other programs.</li></ul>

<p>Knowing this sort of thing is useful when the abstractions break (e.g. when you open a text file with the wrong encoding) or when thinking about the fundamental contours of a system&#39;s possibility space. For example, if you know that programs can be configured with environment variables and CLI flags, knowing that the Python interpreter is a program means you know where to start looking if you need to configure it.</p>

<p>While there are many, many articles on the internet explaining how and why to <em>use</em> them, there&#39;s less information about what a Python virtual environment (or “virtualenv”) <em>is</em>. Luckily for us, it ends up not being very difficult to investigate.</p>



<h2 id="looking-inside-a-virtualenv" id="looking-inside-a-virtualenv">Looking inside a virtualenv</h2>

<p>Creating a virtualenv is the <a href="https://docs.python.org/3/library/venv.html#creating-virtual-environments">work of a moment</a>. If you have Python installed, you can get one of your very own like so:</p>

<pre><code class="language-shell">&gt; python -m venv ./venv
</code></pre>

<p>This creates a directory called <code>venv</code>. Let&#39;s see what&#39;s in it!</p>

<pre><code class="language-shell">&gt; cd venv
&gt; ls
...
</code></pre>

<p>Listing the directory&#39;s contents reveals three directories and a file:</p>
<ul><li><code>Include/</code>, which appears to be empty,</li>
<li><code>Lib/</code>, which contains a directory called <code>site-packages</code>,</li>
<li><code>Scripts/</code>, which contains a bunch of executables, including the Python interpreter and the activate/deactivate scripts for this virtualenv, and</li>
<li><code>pyvenv.cfg</code>, a config file with the Python version the path to my system Python interpreter.</li></ul>

<p>(Note: This was run on a Windows machine; you&#39;ll get different-but-analogous results if you run it on Linux or MacOS).</p>

<p>I know from prior experience that <code>site-packages</code> is where Python packages get installed, that “include” is probably something to do with compiled extensions. It also makes sense that there would be a central place to put scripts, so that the virtualenv&#39;s activation script can add it to your shell&#39;s PATH. So a virtualenv is a directory full of things for Python to import or execute. But how does it <em>work</em>?</p>

<h2 id="understanding-virtualenvs" id="understanding-virtualenvs">Understanding virtualenvs</h2>

<p>The documentation of virtualenvironments is, again, very helpful if you want to understand how to use a virtualenvironment, but not particularly illuminating on the topic of its inner workings.</p>

<p>Instead the answer is in the documentation for the <a href="https://docs.python.org/3/library/site.html?highlight=pyvenv%20cfg"><code>site</code> package</a>: it turns out that when the Python interpreter starts up it looks for a <code>pyvenv.cfg</code> file one directory above itself. If it finds one, it knows its in a virtualenv and configures itself accordingly.</p>

<p>Now we know a few things about virtualenvs:</p>
<ul><li><p>You can use the Python interpreter at <code>venv/scripts/python</code> without activating the virtualenv.</p></li>

<li><p>If you need to find the source code of a dependency (for debugging, say) you can find it in <code>venv/Lib/site-packages/</code>.</p></li>

<li><p>You can ruin your virtualenv by messing with the <code>pyvenv.cfg</code> file (or, possibly, un-mess it to fix a ruined virtualenv).</p></li></ul>
]]></content:encoded>
      <guid>http://natknight.xyz/whats-in-a-virtualenv</guid>
      <pubDate>Tue, 04 May 2021 07:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Consistent Random UUIDs in Python</title>
      <link>http://natknight.xyz/consistent-random-uuids-in-python</link>
      <description>&lt;![CDATA[#python #testing&#xA;&#xA;When I&#39;m doing data analysis or building applications with Python and I have to give entities a unique ID, I like to use random UUIDs instead of sequential numbers. Sequential numbers include information about the order and total number of data, but I want my IDs to be just a unique identifier, nothing more.&#xA;&#xA;[uuid-def]: https://en.wikipedia.org/wiki/Universallyuniqueidentifier&#xA;&#xA;!--more--&#xA;&#xA;Python&#39;s standard library includes the uuid module, for working with UUIDs. There&#39;s a convenient function for generating random ones:&#xA;&#xA;[uuid-module]: https://docs.python.org/3.7/library/uuid.html&#xA;&#xA;      import uuid&#xA;      uuid.uuid4()&#xA;UUID(&#39;189afb2c-1d58-4390-b35e-d5c0e3bb7472&#39;)&#xA;      uuid.uuid4()&#xA;UUID(&#39;0fde2d22-1918-4e39-8c6c-825c2655cbd5&#39;)&#xA;&#xA;Handy. 🙂&#xA;&#xA;I like my UUIDs random, but it can be useful to have them be consistent between runs. That way, you can re-run data processing scripts but keep the same UUIDs. This is a little trickier than I thought it would be, but it&#39;s certainly possible.&#xA;&#xA;My first instinct was to set the random seed in the random module, which is usually enough to make the random number generator behave the same way with each run:&#xA;&#xA;[random-module]: https://docs.python.org/3.7/library/random.html&#xA;&#xA;      import random&#xA;      random.seed(&#34;peanutbutter&#34;)&#xA;      [random.randint(0, 100) for  in range(12)]&#xA;[79, 83, 26, 76, 3, 4, 2, 12, 22, 75, 34, 15]&#xA;      random.seed(&#34;peanutbutter&#34;)&#xA;      [random.randint(0, 100) for  in range(24)]&#xA;[79, 83, 26, 76, 3, 4, 2, 12, 22, 75, 34, 15]&#xA;&#xA;Setting the seed makes random.randint give us consistent results. Maybe it will work for uuid.uuid4 as well.&#xA;&#xA;      import uuid&#xA;      import random&#xA;      random.seed(&#34;peanutbutter&#34;)&#xA;      uuid.uuid4()&#xA;UUID(&#39;37b88a25-9f0f-4308-9532-84fd9a924c06&#39;)&#xA;      random.seed(&#34;peanutbutter&#34;)&#xA;      uuid.uuid4()&#xA;UUID(&#39;04961188-33db-4ad9-86da-e9fcfc6a22e1&#39;)&#xA;&#xA;Huh. That didn&#39;t work. What gives? 🤔&#xA;&#xA;Let&#39;s look at the source code for uuid.uuid4:&#xA;&#xA;def uuid4():&#xA;    &#34;&#34;&#34;Generate a random UUID.&#34;&#34;&#34;&#xA;    return UUID(bytes=os.urandom(16), version=4)&#xA;&#xA;We can see that it&#39;s using os.urandom instead of the random module. That function goes straight to the operating system&#39;s random number generator, which isn&#39;t affected by random.seed. That&#39;s definitely a good thing! The random module is a pseudo random number generator, and using it in some kinds of application could cause security vulnerabilities, so for those applications, os.urandom is the right choice.&#xA;&#xA;[os-urandom]: https://docs.python.org/3.7/library/os.html#os.urandom&#xA;&#xA;However, it&#39;s not what I want for my data analysis application, so how can we make it use a pseudo-random generator instead?&#xA;&#xA;We can see that uuid.uuid4 is making uuid.UUID objects. If we can provide our own, pseudo-random bytes, we can generate pseudo-random UUIDs instead.&#xA;&#xA;[uuid-class]: https://docs.python.org/3.7/library/uuid.html#uuid.UUID&#xA;&#xA;uuid.UUID wants a bytes object, which we can make from a sequence of integers, like this:&#xA;&#xA;[bytes-obj]: https://docs.python.org/3/library/stdtypes.html#bytes&#xA;&#xA;      integers = [1, 2, 4, 8, 16]&#xA;      bytes(integers)&#xA;b&#39;\x01\x02\x04\x08\x10&#39;&#xA;&#xA;We can get a sequence of random, 8-bit integers (i.e. bytes) from random using the random.getrandbits function&#xA;&#xA;[getrandbits]: https://docs.python.org/3.7/library/random.html#random.getrandbits&#xA;&#xA;Putting it all together, we get something like this:&#xA;&#xA;      import random&#xA;      import uuid&#xA;      def randomuuid():&#xA;...     return uuid.UUID(bytes=bytes(random.getrandbits(8) for  in range(16)), version=4)&#xA;...&#xA;      random.seed(&#34;peanutbutter&#34;)&#xA;      randomuuid()&#xA;UUID(&#39;dad39ff6-a734-4906-8804-182dda97441f&#39;)&#xA;      random.seed(&#34;peanutbutter&#34;)&#xA;      randomuuid()&#xA;UUID(&#39;dad39ff6-a734-4906-8804-182dda97441f&#39;)&#xA;&#xA;Success! 🎉&#xA;&#xA;That&#39;s how to generate consistent (pseudo) random UUIDs with Python&#39;s standard library.&#xA;&#xA;One final note: the code above uses the shared random number generator in the random module, so if you need independent sequences of random UUIDs (e.g. for running isolated tests in parallel) it might be better to use separate random number generators for each sequence. I&#39;ve written up a sample implementation of how to do that, which is available here.&#xA;&#xA;[uuid-gen-snippet]: https://bitbucket.org/snippets/nathanielknight/aez955/consistent-isolated-random-uuid-ngenerator&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:python" class="hashtag"><span>#</span><span class="p-category">python</span></a> <a href="http://natknight.xyz/tag:testing" class="hashtag"><span>#</span><span class="p-category">testing</span></a></p>

<p>When I&#39;m doing data analysis or building applications with Python and I have to give entities a unique ID, I like to use <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">random UUIDs</a> instead of sequential numbers. Sequential numbers include information about the order and total number of data, but I want my IDs to be just a unique identifier, nothing more.</p>



<p>Python&#39;s standard library includes the <a href="https://docs.python.org/3.7/library/uuid.html"><code>uuid</code> module</a>, for working with UUIDs. There&#39;s a convenient function for generating random ones:</p>

<pre><code class="language-python">&gt;&gt;&gt; import uuid
&gt;&gt;&gt; uuid.uuid4()
UUID(&#39;189afb2c-1d58-4390-b35e-d5c0e3bb7472&#39;)
&gt;&gt;&gt; uuid.uuid4()
UUID(&#39;0fde2d22-1918-4e39-8c6c-825c2655cbd5&#39;)
</code></pre>

<p>Handy. 🙂</p>

<p>I like my UUIDs random, but it can be useful to have them be consistent between runs. That way, you can re-run data processing scripts but keep the same UUIDs. This is a little trickier than I thought it would be, but it&#39;s certainly possible.</p>

<p>My first instinct was to set the random seed in the <a href="https://docs.python.org/3.7/library/random.html"><code>random</code> module</a>, which is usually enough to make the random number generator behave the same way with each run:</p>

<pre><code class="language-python">&gt;&gt;&gt; import random
&gt;&gt;&gt; random.seed(&#34;peanutbutter&#34;)
&gt;&gt;&gt; [random.randint(0, 100) for _ in range(12)]
[79, 83, 26, 76, 3, 4, 2, 12, 22, 75, 34, 15]
&gt;&gt;&gt; random.seed(&#34;peanutbutter&#34;)
&gt;&gt;&gt; [random.randint(0, 100) for _ in range(24)]
[79, 83, 26, 76, 3, 4, 2, 12, 22, 75, 34, 15]
</code></pre>

<p>Setting the seed makes <code>random.randint</code> give us consistent results. Maybe it will work for <code>uuid.uuid4</code> as well.</p>

<pre><code class="language-python">&gt;&gt;&gt; import uuid
&gt;&gt;&gt; import random
&gt;&gt;&gt; random.seed(&#34;peanutbutter&#34;)
&gt;&gt;&gt; uuid.uuid4()
UUID(&#39;37b88a25-9f0f-4308-9532-84fd9a924c06&#39;)
&gt;&gt;&gt; random.seed(&#34;peanutbutter&#34;)
&gt;&gt;&gt; uuid.uuid4()
UUID(&#39;04961188-33db-4ad9-86da-e9fcfc6a22e1&#39;)
</code></pre>

<p>Huh. That didn&#39;t work. What gives? 🤔</p>

<p>Let&#39;s look at the source code for <code>uuid.uuid4</code>:</p>

<pre><code class="language-python">def uuid4():
    &#34;&#34;&#34;Generate a random UUID.&#34;&#34;&#34;
    return UUID(bytes=os.urandom(16), version=4)
</code></pre>

<p>We can see that it&#39;s using <a href="https://docs.python.org/3.7/library/os.html#os.urandom"><code>os.urandom</code></a> instead of the <code>random</code> module. That function goes straight to the operating system&#39;s random number generator, which isn&#39;t affected by <code>random.seed</code>. That&#39;s definitely a good thing! The <code>random</code> module is a pseudo random number generator, and using it in some kinds of application could cause security vulnerabilities, so for those applications, <code>os.urandom</code> is the right choice.</p>

<p>However, it&#39;s not what I want for my data analysis application, so how can we make it use a pseudo-random generator instead?</p>

<p>We can see that <code>uuid.uuid4</code> is making <a href="https://docs.python.org/3.7/library/uuid.html#uuid.UUID"><code>uuid.UUID</code></a> objects. If we can provide our own, pseudo-random bytes, we can generate pseudo-random UUIDs instead.</p>

<p><code>uuid.UUID</code> wants a <a href="https://docs.python.org/3/library/stdtypes.html#bytes"><code>bytes</code> object</a>, which we can make from a sequence of integers, like this:</p>

<pre><code class="language-python">&gt;&gt;&gt; integers = [1, 2, 4, 8, 16]
&gt;&gt;&gt; bytes(integers)
b&#39;\x01\x02\x04\x08\x10&#39;
</code></pre>

<p>We can get a sequence of random, 8-bit integers (i.e. bytes) from <code>random</code> using the <a href="https://docs.python.org/3.7/library/random.html#random.getrandbits"><code>random.getrandbits</code> function</a></p>

<p>Putting it all together, we get something like this:</p>

<pre><code class="language-python">&gt;&gt;&gt; import random
&gt;&gt;&gt; import uuid
&gt;&gt;&gt; def random_uuid():
...     return uuid.UUID(bytes=bytes(random.getrandbits(8) for _ in range(16)), version=4)
...
&gt;&gt;&gt; random.seed(&#34;peanutbutter&#34;)
&gt;&gt;&gt; random_uuid()
UUID(&#39;dad39ff6-a734-4906-8804-182dda97441f&#39;)
&gt;&gt;&gt; random.seed(&#34;peanutbutter&#34;)
&gt;&gt;&gt; random_uuid()
UUID(&#39;dad39ff6-a734-4906-8804-182dda97441f&#39;)
</code></pre>

<p>Success! 🎉</p>

<p>That&#39;s how to generate consistent (pseudo) random UUIDs with Python&#39;s standard library.</p>

<p>One final note: the code above uses the shared random number generator in the <code>random</code> module, so if you need independent sequences of random UUIDs (e.g. for running isolated tests in parallel) it might be better to use separate random number generators for each sequence. I&#39;ve written up a sample implementation of how to do that, which is available <a href="https://bitbucket.org/snippets/nathanielknight/aez955/consistent-isolated-random-uuid-ngenerator">here</a>.</p>
]]></content:encoded>
      <guid>http://natknight.xyz/consistent-random-uuids-in-python</guid>
      <pubDate>Wed, 14 Nov 2018 08:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>