<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>rhelmot blog</title><link href="https://blog.rhelmot.io/"/><link href="https://blog.rhelmot.io//atom/index.xml" rel="self"/><updated>2026-08-29 00:00-0700</updated><entry><title>It's a python module world, we're just living in it</title><link rel="alternate" type="text/html" href="https://blog.rhelmot.io/post/python-imports/"/><published>2026-08-29 00:00-0700</published><summary>How the import statement and the python module system interact</description><content type="html"><p>I have spent the better part of 14 years writing Python for fun and profit.
I don&#8217;t recommend anyone does this, especially if you&#8217;re just starting off learning to program, but (joker voice) we live in a society, so it falls on our shoulders to explain how to do this effectively.</p><p>The<code>import</code> statement is the powerhouse of the Python ecosystem.
You categorically need it or something like it if you&#8217;re going to do anything complicated in the language.
Unfortunately, it has some deeply unintuitive characteristics which I struggled with for a long time and which I have struggled to explain to my peers and pupils.
This post attempts to de-mystify what happens when you type this out.</p><p>The environment variable<code>PYTHONPATH</code> controls where Python will look for crap when you give it an unqualified import statement.
Valid python modules are:</p><p>1) a .py file, wherein you can import<code>foo.py</code> with<code>import foo</code> or,
2) a directory with an<code>__init__.py</code> file, wherein you can import<code>foo/__init__.py</code> with<code>import foo</code>, and,
3) some more bullshit that doesn&#8217;t matter</p><p>The import statement and its cousin, the from-import statement, do two things:</p><p>1) they perform the actual import operation which involves crawling the filesystem for code files and executing them to generate module objects in memory, and,
2) they take some aspect of the result of that operation and assigns one or more variables to the current namespace (the one in which you actually typed the import statement).</p><p>These two phases are best explained separately, but they are part of the same statement and thus have some odd interactions.
Let&#8217;s look at them one at a time.</p><h2 id="thing1:finditandrunit">Thing 1: find it and run it</h2><p>The<code>xxx</code> in<code>import xxx</code> or<code>from xxx import whatever</code> is the<em>specification</em> for the first part of this process.
When you ask the Python interpreter to do this, it will start looking through all the directories named on<code>$PYTHONPATH</code> (which often includes the current working directory) and scan for an<code>xxx.py</code> or<code>xxx/__init__.py</code>.
This file becomes the target of the import. If you say<code>import xxx.yyy</code> or<code>from xxx.yyy import whatever</code>, then it will be looking for<code>xxx/yyy.py</code> or<code>xxx/yyy/__init__.py</code>.</p><p>If you&#8217;re already inside some sort of module when you type the import statement, you gain access to relative imports.
These start with a dot.
Some examples:</p><ul><li> if you&#8217;re in<code>xxx/yyy.py</code> you can say<code>from . import whatever</code> and this will refer to<code>xxx/__init__.py</code></li><li> if you say<code>from .zzz import whatever</code> it will refer to<code>xxx/zzz.py</code> or<code>xxx/zzz/__init__.py</code></li></ul><p>You can use two dots to go up a level and so forth:</p><ul><li> while you&#8217;re in<code>xxx/yyy/zzz.py</code> you can say<code>from ..aaa import whatever</code> and will refer to<code>xxx/aaa.py</code> or<code>xxx/aaa/__init__.py</code></li><li> you can even say<code>from ..aaa.bbb import whatever</code> and it will refer to<code>xxx/aaa/bbb.py</code> or<code>xxx/aaa/bbb/__init__.py</code>.</li></ul><p>Note that because<code>xxx/yyy.py</code> can do things with these relative imports, it doesn&#8217;t make sense to try to run it until Python has successfully executed/loaded<code>xxx/__init__.py</code>.
It&#8217;ll do this automatically in the background if it needs to.</p><p>This reveals something interesting about the Python module system - you don&#8217;t necessarily have to import a whole package all at once, and some packages in fact want you to pick and choose which parts you import!
If you<code>pip install sillylib</code> and that causes<code>silly/__init__.py</code> shows up on your<code>$PYTHONPATH</code>, even if<code>silly/billy.py</code><em>also</em> shows up on<code>$PYTHONPATH</code>, you may not be able to say<code>import silly</code> and then access<code>silly.billy</code>!
This depends on whether<code>silly/__init__.py</code> actually includes a<code>from . import billy</code>.
It may omit this on purpose!
In this case, you will need to explicitly say<code>import silly.billy</code>.</p><h2 id="thing2:messwiththecurrentnamespace">Thing 2: mess with the current namespace</h2><p>Python modules, from a post-import usage perspective, are just objects.
If you write a file<code>foo.py</code> that contains<code>x = 42</code>, you can say<code>import foo</code> and then access<code>foo.x</code>.
The actual mechanics of this is that<code>foo</code> in the current namespace, that is, the place you actually typed<code>import foo</code>, has had a varible bound in it with name<code>foo</code> that points to the module object which results from executing<code>foo.py</code> and gathering up all its assignments.</p><p>That&#8217;s the plain<code>import xxx</code> statement.
When you say<code>from foo import x</code>, it&#8217;ll load the module associated with the name &#8220;foo&#8221;, grab the attribute<code>x</code> out of that module, and then assign that to the name<code>x</code> in the current namespace.
The<code>as</code> clause, which we have not mentioned so far in this tutorial but you may have seen before in the wild, adjusts this behavior - you can say<code>import foo as foo2</code> or<code>from foo import x as blah</code> to change which name the module or value-within-module gets bound to in the current namespace.</p><p>Horrifyingly, there is one very stupid edge case to how these two phases interact.
When you use a dotted import path in the plain<code>import xxx.yyy</code> statement, it will actually put just<code>xxx</code> into your namespace and instead put<code>yyy</code> as an attribute of xxx, in order to satisfy the case we talked about earlier where<code>silly</code> doesn&#8217;t want to load its<code>billy.py</code> by default.
HOWEVER, if you say<code>import xxx.yyy as zzz</code> then it will put the module-object resulting from executing<code>xxx/yyy.py</code> as the variable<code>zzz</code> in the current namespace!!!!!
This is the way it is because it serves all the normal use-cases in a compact syntax but I go insane every time I try to explain it.
Truly a victory for the semantic compressor at the expense of everyone else.</p><h2 id="conclusion">Conclusion</h2><figure><img src="bad.webp" alt="The survivability onion meme, but the layers are relabeled to say: don't use a computer, don't write code, don't write python, don't write bad python, don't get mad about bad python, don't write blog posts about bad python"/><figcaption>The survivability onion meme, but the layers are relabeled to say: don&#8217;t use a computer, don&#8217;t write code, don&#8217;t write python, don&#8217;t write bad python, don&#8217;t get mad about bad python, don&#8217;t write blog posts about bad python</figcaption></figure></content><author><name>Audrey Dutcher</name></author></entry><entry><title>A beginner's guide to NixOS channels vs flakes</title><link rel="alternate" type="text/html" href="https://blog.rhelmot.io/post/nix-channels-and-flakes/"/><published>2025-10-06 00:00-0700</published><summary>An overview of how to manage your NixOS system configuration, specifically how to manage which version of nixpkgs you're using</description><content type="html"><p>The goal here is to answer the question &#8220;how does your system know what version of nixpkgs to use&#8221;, i.e. &#8220;how do you tell what version of firefox I am going to get when I ask for firefox to be installed&#8221;.</p><p>There are two approaches, channels and flakes. The long and the short of this is that channels are a mechanism baked into your system<a href="#fn:1" id="fnref:1" title="see footnote" class="footnote"><sup>1</sup></a> and flakes are a mechanism stored in a file next to your configuration.</p><p>I do not know of a version of managing either of these that doesn&#8217;t involve using the terminal the entire time.</p><h1 id="channels">Channels</h1><p>To manage channels, you will use the<code>nix-channel</code> command in the terminal. You will need to run it as the root (administrator) user, i.e. prefix the command with<code>sudo</code>. This can be confusing because if you run<code>nix-channel</code> as your non-privileged user, i.e. the default when you don&#8217;t prefix with<code>sudo</code>, it will still do something, but that something will almost certainly not be what you want!</p><p>Start with<code>sudo nix-channel --list</code>. This will list all the channels that are being managed by your system. Usually there is just one, named &#8220;nixos&#8221;. You can see that it probably points at something like https://nixos.org/channels/nixos-25.05.</p><p>If NixOS 25.11 were to come out tomorrow, you would just change the 25.05 to 25.11 and you would get all the shiny new software. You can do this with<code>sudo nix-channel --add https://nixos.org/channels/nixos-25.11 nixos</code>.</p><p>If you simply want to do a light upgrade and get only the software vetted for the 25.05 release but with any important fixes that have been discovered since release, you just want<code>sudo nix-channel --update nixos</code>. This will check if there is a new version of 25.05 available and switch to it if there is. Weird that even though there&#8217;s a version number, there&#8217;s subversions of that version&#8230;</p><h1 id="flakes">Flakes</h1><p>Flakes are the alternative which doesn&#8217;t bake anything into your system, and are in fact philosophically designed to avoid having to bake anything into your system. The &#8220;goal&#8221; per se is to make it so that you can share a folder of files and suddenly your system is fully 100% reproducable, locking in place the version of nixpkgs (and potentially more things!) in a way that is simply just in a text file and is easily sharable.</p><p>These text files are named flake.nix and flake.lock. flake.nix is a file that you can edit, technically in the same language as your configuration.nix file but using some more arcane features. flake.lock is still a text file but you are not meant to edit it by hand. Instead, you run the<code>nix flake update</code> command, and the flake.lock file is automatically regenerated with the newest version of everything.</p><p>Writing a flake.nix file is a little weird, you would probably just want to copy one from someone else online. Maybe start with<a href="#fn:2" id="fnref:2" title="see footnote" class="footnote"><sup>2</sup></a> I don&#8217;t think I can explain how to write one from first principles in just this document. However, once you find one, it will also contain a line which basically says &#8220;nixpkgs is going to be a version named 25.05&#8221;. If you edit this file to say 25.11, your next system build will use 25.11.</p><p>One of the other benefits of using flakes is that you can put your configuration.nix and hardware-configuration.nix files anywhere, not just in /etc/nixos. You will have to put them in the same folder of flake.nix and flake.lock (or in a folder next to those files), but you can put the folder containing all that crap literally wherever.</p><p>One final caveat is that if you have a git repository that contains your flake files, nix will ignore files which are in that folder but not tracked by git. This doesn&#8217;t matter unless you start using git on the command line :)</p><h1 id="atablecomparingterminalcommands">A table comparing terminal commands</h1><table><colgroup><col/><col/><col/></colgroup><thead><tr><th> Goal</th><th> Channels solution</th><th> Flakes solution</th></tr></thead><tbody><tr><td> Switch to the most recent release of your current version of nixos</td><td><code>sudo nix-channel --update nixos</code></td><td><code>nix flake update</code> (Automatically edits flake.lock)</td></tr><tr><td> Switch to a different release version of nixos</td><td><code>sudo nix-channel add ... nixos</code></td><td> Edit flake.nix</td></tr><tr><td> Build a configuration and make your system use it</td><td><code>sudo nixos-rebuild switch</code><a href="#fn:3" id="fnref:3" title="see footnote" class="footnote"><sup>3</sup></a></td><td><code>sudo nixos-rebuild switch --flake path/to/folder-that-has-flake-stuff</code></td></tr></tbody></table><h1 id="thedirtydetails">The dirty details</h1><p>The nix programming language has a weird escape hatch for accessing external resources at evaluation time. If you ever see nix code that looks like<code>with import &lt;nixpkgs&gt; {}; blah blah</code>, this is using this escape hatch! This syntax, the import with angle brackets, refers to an &#8220;impure&#8221; resource, i.e, some sort of external truth which dictates what code you actually mean by nixpkgs. This is widely considered to be a bit of a wart on the language&#8217;s beautiful face! People love nix for its purity (technical term) and its reproducability, and this capability shoots that down immediately. Flakes are designed to be the solution to this - the angle brackets import syntax simply will not work in the &#8220;pure&#8221; evaluation mode, which is used when working with flakes. Instead, the nix interpreter understands the special structure of the flake.nix and flake.lock files and reads these in order to populate a list of inputs, which it provides to code running in the flake.nix file (outputs). In this guide, nixpkgs has been the only input, but you can use lots of other things as inputs! Random github repositories that have a flake.nix if you like!</p><p>The flake.nix structure is actually a very extensible structure that can be used for lots of things other than a NixOS system configuration. Take a look at<a href="#fn:4" id="fnref:4" title="see footnote" class="footnote"><sup>4</sup></a> for all the things that can be done :)</p><div class="footnotes"><hr/><ol><li id="fn:1"><p>Associated with the root account, in /nix/var/nix/profiles/per-user/root/channels<a href="#fnref:1" title="return to body" class="reversefootnote">&#160;&#8617;&#xfe0e;</a></p></li><li id="fn:2"><p><a href="https://wiki.nixos.org/wiki/NixOS_system_configuration#Defining_NixOS_as_a_flake">https://wiki.nixos.org/wiki/NixOS_system_configuration#Defining_NixOS_as_a_flake</a><a href="#fnref:2" title="return to body" class="reversefootnote">&#160;&#8617;&#xfe0e;</a></p></li><li id="fn:3"><p>This will by default use /etc/nix/configuration.nix<a href="#fnref:3" title="return to body" class="reversefootnote">&#160;&#8617;&#xfe0e;</a></p></li><li id="fn:4"><p><a href="https://wiki.nixos.org/wiki/Flakes">https://wiki.nixos.org/wiki/Flakes</a><a href="#fnref:4" title="return to body" class="reversefootnote">&#160;&#8617;&#xfe0e;</a></p></li></ol></div></content><author><name>Audrey Dutcher</name></author></entry><entry><title>Hello Blog - How I streamlined my application management with Nix</title><link rel="alternate" type="text/html" href="https://blog.rhelmot.io/post/hello/"/><published>2025-04-20 00:00-0700</published><summary>The obligatory blog boot-up post about how I built the blog</description><content type="html"><p>As you know if you&#8217;re part of my life, I have been going pretty hard on nixing my workstations and servers lately.
This post is to commemorate the creation of this blog being run through nix, but also the workflow I&#8217;ve established to get here.</p><p>Since the year is 2025 and attention span is in short supply, here&#8217;s the punchline:</p><ul><li>I ran<code>nix run .#sunflower.deploy</code> to update my server, including deploying this blog</li><li>I will run<code>nix run .#sunflower.deploy.blog-rhelmot-io</code> to update the blog without updating the system profile</li><li>&#8230;but I can still roll the blog forward and back without rolling the system forward and back, because the blog is its own nix profile!</li></ul><p>Let&#8217;s break it down.</p><h1 id="part1:staticsitegeneration">Part 1: Static Site Generation</h1><p>What a hotly contentious topic. I could probably stand to care a little bit less, but the caring I do care isn&#8217;t much.
I started this journey trying to use Jekyll, but quickly found that the Ruby-isms were too much for me.
I did at one point succeed at getting<a href="https://git.lain.faith/rhelmot/blog.rhelmot.io/commit/5e24401e67b613c7d81abcec8aed14fdf04a4159">a flake output which could build a whole Jekyll site</a>, but it was too much of a hack for me to deign to put it upon my domain.</p><p>I eventually found<a href="https://github.com/danth/coricamu">Coricamu</a>, which is exactly what I want, though it seems to be abandoned.
It&#8217;s small enough that I feel comfortable carrying it on my shoulders, so I forked it and did the one small fix necessary to get it to work with current nixpkgs.
I may mess around with theming later, but it provides exactly what I want and not much more.</p><p>The source for this blog can be found<a href="https://git.lain.faith/rhelmot/blog.rhelmot.io">here</a>.</p><p>Coricamu lets you build a site through a NixOS-style module system.
I constructed my flake.nix such that it runs module evaluation with two modules:</p><ul><li>the<code>blog.nix</code> file, defining the site metadata</li><li>a module constructed automatically by slapping each entry of<code>/posts/*/post.nix</code> into a post directive</li></ul><pre><code class="nix">let
posts = let
listingMap = builtins.readDir ./posts;
listing = builtins.attrNames listingMap;
getPostFile = post: (import ./posts/${post}/post.nix) // { slug = post; };
in builtins.map getPostFile listing;
in
coricamu.lib.generateFlakeOutputs {
outputName = "blog";
modules = [ ./blog.nix { inherit posts; }];
};</code></pre><p>Then I just write some posts in markdown, put some quick metadata into a .nix file which references the markdown file, and build:<code>nix build .#blog</code>.
I can also<code>nix run .#blog-preview</code> if I want a fancy server.
Truly, we stand on the shoulders of giants.</p><h1 id="part2:managingmultiplemachines">Part 2: Managing Multiple Machines</h1><p>I have several machines I manage with a<a href="https://git.lain.faith/rhelmot/nixos-config">central NixOS configuration repository</a> - some workstations and some servers.
This flake.nix also does a directory scan in order to populate its outputs, this time scanning<code>sites</code> in order to populate<code>packages.${buildSystem}.nixosConfigurations.${site}</code>.</p><p>Yes,<code>nixos-rebuild</code> will automatically search<code>packages.${buildSystem}</code> in order to build a system, allowing for cross compilation.
This fact is particularly useful seeing as I work on<a href="https://github.com/nixos-bsd/nixbsd">NixBSD</a> and am constantly cross compiling entire systems.
There are various accoutrements in this repository which make it reasonable for me to use it for both NixOS and NixBSD systems, but that&#8217;s a story for another time.</p><p>Now, I can deploy any of these systems with<code>nixos-rebuild .#$HOST --remote-target $HOST --use-remote-sudo switch</code>, ideally with<code>--use-substitutes</code> since my home internet uplink is dogshit.</p><p>There is a problem though - if I want to have my blog as a nix derivation, this means that I have to run a full system rebuild every time I publish a new post.
It is very easy to simply drop the derivation output into the nginx configuration, but suddenly rollbacks are tied together with both the system and blog. Can we do better?</p><h1 id="part3:profilesanddeployment">Part 3: Profiles and Deployment</h1><p>Yes, we can, with the power of<a href="https://nix.dev/manual/nix/2.24/command-ref/files/profiles">Nix Profiles</a>!
We won&#8217;t be linking the typical kind of derivation output you would usually be putting in a user profile, with the system path and applications and such.
Instead, our profile will simply be the static site build output derivation!</p><p>I may in the future decide to standardize some sort of &#8220;nginx site derivation&#8221; layout so I can link non-static sites this way, but for now I just point the root of the site at<code>/nix/var/nix/profiles/blog-rhelmot-io</code>, and deploy as follows:</p><pre><code class="shell">nix-copy-closure --to $SITE $DRV
ssh $SITE sudo nix-env --set -p /nix/var/nix/profiles/blog-rhelmot-io $DRV</code></pre><p>This can be automated! Check<a href="https://git.lain.faith/rhelmot/nixos-config/src/branch/main/deploy.nix">deploy.nix</a> in my NixOS configuration for the final product.
I define a list of deployments, each of which sets a profile name, a site to deploy on, and the package to deploy:</p><pre><code class="nix">deployments = builtins.map mkDeploy [
{
profileName = "blog-rhelmot-io";
site = "sunflower";
targetPkg = flakeInputs."blog-rhelmot-io".packages.${platform}.blog;
}
];</code></pre><p><code>mkDeploy</code> simply templates the previously-mentioned script with these parameters.
We can then combine each script for a given site into a unified deploy script, along with a system profile rebuild for good measure:</p><pre><code class="nix">filteredDeployments = builtins.filter (deployment: deployment.site == site) deployments;
targetSystem = flakeInputs.self.packages.${platform}.${site}.system;
deployAll = pkgs.writeShellScriptBin "deploy-all-${site}" (''
set -ex
# TODO take advantage of the nixos-rebuild infrastructure
nix-copy-closure --to ${site} ${targetSystem}
ssh ${site} 'sudo nix-env --set -p /nix/var/nix/profiles/system ${targetSystem} &amp;&amp; sudo ${targetSystem}/bin/switch-to-configuration switch'
set +e
'' + lib.concatStringsSep "\n" filteredDeployments);</code></pre><p>It is annoying that<code>nixos-rebuild</code> proper doesn&#8217;t support this use-case - that is, deploying a pre-built system profile.
I have made the requisite change to enable this behavior in the<a href="https://github.com/nixos-bsd/nixbsd/blob/main/modules/installer/tools/nixos-rebuild.sh">NixBSD fork of nixos-rebuild</a>, but this script is obviously only appropriate for building BSD targets.</p><p>Finally, we can deploy just the blog with a sub-attribute,<code>nix run .#sunflower.deploy.blog-rhelmot-io</code>:</p><pre><code class="nix">filteredDeploymentsAttrs = builtins.listToAttrs (builtins.map (value: { name = value.profileName; inherit value; }) filteredDeployments);
final = deployAll // filteredDeploymentsAttrs;</code></pre><p>I believe this is the best of both worlds.</p><h1 id="conclusion">Conclusion</h1><p>Yippee woo hoo ya ha ha</p><p>You too can take control of your systems like this. Go forth and nixify!</p></content><author><name>Audrey Dutcher</name></author></entry></feed>