The Manual

Python

Two different people open a Python guide on NixOS, and most guides answer only one of them.

Someone following a tutorial wants pip install to work, right now, in whatever directory the tutorial says. That is not a packaging problem and it does not need a flake – it needs a virtualenv, ordinary wheels from PyPI, and a loader that can find the libraries those wheels were built against. This page serves that person first.

Someone building a project wants the environment to be the same on every machine, next year. That person gets the second half of the page: the python preset from per-project environments, uv, and where Nix-built Python actually earns its keep.

I just want pip to work

python3 is already on every nixarchy machine – it is a dependency of Omarchy’s own scripts, and Install ▸ Development ▸ Python makes it part of your configuration explicitly. So the tutorial path is the tutorial path:

python3 -m venv .venv
source .venv/bin/activate
pip install requests

This works, unmodified, for most of PyPI. A pure-Python package – requests, flask, click, most of what a tutorial asks for – installs and imports on NixOS exactly as it does anywhere else. Nothing about the venv-and-wheels workflow is wrong here, and nothing below asks you to give it up.

The error this page exists for

Then you install a wheel with compiled code in it, pip install succeeds – and on a bare NixOS (or for a library nixarchy’s shipped set does not carry) the import dies:

ImportError: libGL.so.1: cannot open shared object file: No such file or directory

Read that error precisely, because it names the real problem. The wheel is fine. pip is fine. Your venv is fine. A wheel with compiled code in it is an ordinary Linux binary, built on an ordinary distro, and at import time the dynamic loader goes looking for the shared libraries it was linked against – libGL.so.1, libstdc++.so.6, libz.so.1 – in the places ordinary distros keep them: /usr/lib, /lib. NixOS deliberately has no /usr/lib. Every library lives in /nix/store under a versioned path, and only programs built by Nix know where. The binary is asking a reasonable question in a filesystem that answers it for nobody.

So this is a loader problem, not a Python problem. The same failure, with a different library name in it, hits Node native modules, Electron apps, PyTorch and Playwright – anything that ships prebuilt compiled code.

What fixes it

NixOS’s answer to loose prebuilt binaries is nix-ld: a shim at the path binaries expect the loader at, which supplies a configured set of libraries. It is already enabled on every nixarchy machine, and nixarchy curates programs.nix-ld.libraries beyond the NixOS default – libGL, zlib, libstdc++ and the rest of what common wheels load are in the shipped set.

Which interpreter you are running decides whether that helps you, and the distinction is worth thirty seconds because getting it wrong costs a rebuild and a logout for nothing. nix-ld works by being the loader at /lib64/ld-linux-x86-64.so.2; only a binary whose ELF interpreter is that path reaches the shim, and only the shim reads NIX_LD_LIBRARY_PATH. One command tells you which you have:

patchelf --print-interpreter "$(readlink -f "$(command -v python3)")"

For the second case, hand the same libraries over for the one command that needs them:

LD_LIBRARY_PATH="$NIX_LD_LIBRARY_PATH${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" python3 app.py

Per command, not exported globally – a global LD_LIBRARY_PATH is how unrelated Nix programs start failing. The durable answers are a devenv (below), an FHS environment, or simply running the venv on uv’s own Python. Prebuilt binaries is the full story of that set.

When a wheel wants a library the set does not carry – on an interpreter nix-ld can actually reach – the error names it, and the fix is one option in your own configuration; your entries merge with nixarchy’s rather than replacing them:

programs.nix-ld.libraries = with pkgs; [ libpulseaudio ];

Rebuild, log out and back in (NIX_LD_LIBRARY_PATH is set at login), and the same venv – no reinstall – imports. Each new cannot open shared object file is one more entry in that list: the error tells you the library, search tells you the package, and nixarchy-doctor <path-or-command> does the diagnosis for you.

When the project matters more than the tutorial

Once the code is a project – a repo, a colleague, a deadline – pin the environment instead of relying on whatever the machine has:

nixarchy dev init python

This is the python preset from per-project environments (the devenv service, opt-in): a devenv.nix with languages.python.enable and venv.enable, so devenv creates and enters a virtualenv for you and pip install lands in the project rather than in your home directory. The lock file makes it the same Python on every machine that clones the repo.

For dependency management itself, uv is the current answer – uv.lock as the source of truth. And when you need Nix to build the project from that lock, uv2nix exists – whose own documentation says to skip it for day-to-day development and just use uv. That is a useful signal, not a criticism: reach for uv2nix when something must consume the project as a Nix package, not before.

Machine learning, CUDA, and the one thing nix-ld cannot do

PyTorch and friends are the hardest case of everything above, and they are also the case where the advice above stops applying — so it is worth being precise rather than general.

A CUDA or ROCm wheel from PyPI already contains its own CUDA runtime. The only thing it needs from the machine is the driver, libcuda.so.1, which on NixOS is in /run/opengl-driver/lib. That is a LD_LIBRARY_PATH problem, not a nix-ld one.

And the distinction that trips most people:

nix-ld helps an unpatched binary — one built on an ordinary distro, which asks for the loader at /lib64/ld-linux-x86-64.so.2 and reads NIX_LD_LIBRARY_PATH from its environment. A python3 out of nixpkgs is not such a binary. It is patched to use Nix’s own loader and consults neither variable, so growing programs.nix-ld.libraries does nothing for what it imports.

The interpreter that does read them is the one uv downloads for itself. So the working answer for an ML project is uv’s own Python, and nixarchy dev init ml is that in one command — uv, UV_PYTHON_PREFERENCE = "only-managed", and the driver path already set.

For notebooks, nixarchy dev init jupyter is an ordinary devshell built from python3.withPackages. Do not reach for jupyenv (formerly jupyterWith), which every search puts first: it is unmaintained and does not track current nixpkgs.

What still will not work, and why

Honesty about the limits, so the failure you hit next is not a surprise:

The full ladder – which rung for which failure – is the decision table on the Boxes page.