Nix(OS) for Python Developers
Written on
NixOS is an amazing Linux distribution. Especially, if you’re into both DevOps and Free Software, like me. It’s built using Nix, a pure functional package manager, in its entirety with packages maintained by the community in a single, huge source code repository on GitHub.
As of 2025, NixOS is claimed to have the largest number of software packages of the entire planet, so when you’re looking for hardware support and software packaged for your computer, chances are someone will have taken care of it for you already. And with Nix you stay in control, because you define and manage all of your software and configuration in text files, which you typically put under version control. That’s the main reason why NixOS is so great, and at the same time it’s one of the reasons why NixOS is not for everyone.
Why Nixos Is so Hard to Learn
Running NixOS from a live ISO or installing it from a USB pen drive is just as easy as with any modern Linux distribution. But that’s where the easiness stops. Afterwards, you really need to dive deep into Nix expressions and understand how the language is being evaluated.
You won’t go anywhere with questions in the forum, let alone your favorite AI chat. If you ask 5 users about one problem you’ll get 6 different opinions, and the LLMs reflect that confusion. Part of the confusion is due to the fact that NixOS is still evolving: There are traditional “channels” and new “flakes”.
The Nix Language
Nix is a pure functional language. It follows the principles of lambda calculus with reduction operations at its core.
In practice, that means that you start with reading a file that imports other files by calculating (reducing) their content into a variable (or function output). It does that until there is only a single literal or function left, which turns into the final output.
Hint
Don’t worry now about the resources that need to be created or modified in the target system. Simply try to embrace how a functional language works.
A Nix File Contains A Single Thing
Attribute sets and functions are the dominating elements in Nix files. And when you look closely, you’ll notice that any Nix file contains only a single top-level element. The following examples are both valid Nix files and demonstrations of the data types the Nix language provides.
number.nix
42
string.nix
"foo"
boolean.nix
true
path.nix
~/.local/bin
list.nix
[ "foo" "bar" 42 false ]
attribute-set.nix
{ target = /etc/nixos; foo = 42; }
function.nix
x: x + 1
In Python, a lambda expression is defined with the lambda
keyword
followed by a parameter list, a colon (:
) and the expression body.
In Nix, the structure is the same but there is no lambda
keyword.
Functions written that way are anonymous functions and are lazily evaluated
(i.e. they are lambda expressions as long as at least one of the input
elements is not yet resolved).
$ nix eval --file function.nix
«lambda @ /home/nixos/function.nix:1:1»
Nix functions can only have a single argument. Hence, when a function needs more than one parameter, you wrap curly brackets around the arguments (i.e. you use an attribute set). The same is true for the body when it contains more than a single assignment.
function-advanced.nix
{ x, y }: { a = x; b = x + y; }
And voilà, this is what most Nix files in your configuration will look like! The file can contain a single literal of any of the data types shown above, but it will usually be a function or an attribute set.
Using the import
built-in you can read the Nix expression in a file
and evaluate (reduce) it into a variable.
import-a-number.nix
{ y = import ./number.nix; }
$ nix eval --file import-a-number.nix
{ y = 42; }
Nix Module
A module provides structure for our code, which makes it easier to reuse
it elegantly. It is a Nix file containing a function or lambda expression
with a tripartite structure: imports
, options
and config
.
module.nix
{ lib, ... }:
{
imports = [
./file.nix
];
options = {
magicNumber = lib.mkOption {
type = lib.types.number;
default = 3.14;
};
};
config = {
boot.loader.grub.enable = true;
};
}
Very often you will see modules that have neither an options
nor a
config
block. In this case, the declarations all implicitly belong to
config
. When an options
block is used, an explicit config
block is required, though.
If we want to use local variables, e.g. to avoid repetition or for complex
calculations, we can define them using the let
keyword on top of the
function body. This is similar to modern JavaScript, TypeScript and Rust.
variables.nix
{ lib, ... }:
let
foo = "Hello";
in
{
options = {
greeting = lib.mkOption {
type = lib.types.str;
default = foo;
};
};
}
Nix Flake
A flake is a filesystem tree with a flake.nix
file containing an
attribute set with a structure of 3 or more parts (typically
description
, inputs
and outputs
). Flakes are a concept of
modern Nix for packaging Nix code, and are officially said to be
“experimental”, though everyone will tell you to go ahead and use them.
flake.nix
{
description = "A Flake explained using Python concepts";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
nixos-hardware.url = "github:nixos/nixos-hardware?ref=master";
};
outputs = { self, nixpkgs }: {
nixosConfigurations = nixpkgs.lib.mapAttrs mkSystem systems;
};
}
In Python, a pyproject.toml
file is what comes close to the concept of
a Nix flake: It contains metadata, dependencies and handles entry points.
The nix flake
command is comparable to Python’s uv
or poetry
,
and can be used to create a flake file, update and write a lock file to
document the dependencies, and perform various checks.
$ nix flake init
wrote: "/home/nixos/config/flake.nix"
$ nix flake lock
warning: creating lock file '"/home/nixos/config/flake.lock"':
• Added input 'nixpkgs':
...
$ nix flake update
warning: updating lock file '"/home/nixos/config/flake.lock"':
...
$ nix flake check
$ nix flake show
git+file:///home/nixos/config
└───nixosConfigurations
└───example: NixOS configuration
As a Python developer, when you hear “experimental” in the Nix project, think in terms of “Python 2” versus “Python 3”. In fact, the story is somewhat similar and stems from unresolved discussions with no end in sight yet. For now, it looks safe to rely on flakes and use the new and old CLI commands together.
TL;DR – Concepts Summary
Nix | Python |
---|---|
Flake, flake.nix, flake.lock | pyproject.toml, uv.lock |
Module, <filename>.nix | Module, <filename>.py |
Lambda expression, function | Python code (module) |
Resources
- PyCon 25: Nix(OS) for Python Developers (Slides)
- Example NixOS-Config repository (GitLab)
- NixOS-Config generator (Copier template)