The problem
NixOS doesn't follow the Filesystem Hierarchy Standard. There's no /usr/lib in the traditional sense — system libraries live in the Nix store. This is usually great for reproducibility, but it means Python packages compiled inside a uv virtualenv can't find things like BLAS for numpy or libpng and freetype for matplotlib.
I was using uv for per-project Python environments — uv venv, uv sync, the works. And on my old system (Ubuntu), this worked fine. On NixOS, Jupyter kernels running inside those uv-managed venvs would fail at runtime because they couldn't locate system libraries.
This is a well-known issue. The standard advice is: on NixOS, use Nix-managed Python, not pip/venv/uv. At least I think so
The solution: per-project shell.nix files
Instead of fighting it, I switched to per-project nix-shell environments. Each data science project now has a shell.nix that declares its Python environment through nixpkgs, where everything has proper library paths baked in.
Here's what they look like:
{ pkgs ? import (builtins.getFlake "/home/ben/nixos").inputs.nixpkgs { } }:
pkgs.mkShell {
buildInputs = with pkgs; [
python3
python3Packages.pandas
python3Packages.numpy
python3Packages.matplotlib
python3Packages.jupyterlab
python3Packages.seaborn
python3Packages.scikit-learn
python3Packages.openpyxl
];
}
The key detail is builtins.getFlake "/home/ben/nixos" — this pins every project to the same nixpkgs revision that my system uses. One flake.lock to rule them all. If I update my system, all project environments update together. It's essentially uv lock just higher higher in scope
Before and after
Before: uv venv && uv sync to set up, uv run jupyter lab to work. Kernel files pointing into a .venv directory that couldn't access system libraries.
After: nix-shell to enter the environment, jupyter lab runs inside it with full access to NixOS libraries. No .venv directory in the project. No kernel config to maintain.
What changed
- Created
shell.nixinecommerce-sales-profit-analysis,us-consumer-finance-complaints, andnoahs-rug - Removed
uvfrom my NixOShome.packages - Deleted
molten-python.nix— I wasn't using the Molten Neovim kernel anyway, switched to direct Jupyter Lab usage - Dropped the centralized devShell approach I briefly tried — per-project files are more conventional and don't couple project setup to NixOS config
Things I learned along the way
nix-shellreadsshell.nix;nix developrequiresflake.nix. These aren't interchangeable.import <nixpkgs>looks unpinned — versions drift withnix-channel --update. Usingbuiltins.getFlakeavoids this entirely.nixpkgs-fmtformats Nix files, which is handy - im constantly indenting 4 spaces like everythings python!- I considered a pandas 3.0 overlay but nixpkgs' pandas 2.3.3 is sufficient. nixpkgs actually tried pandas 3.0.0 in January 2026 but reverted due to downstream breakage.
blackandjupytextstayed as global Home Manager packages — they're useful outside project shells.
Final thoughts
If you're on NixOS and using Python, the answer is simple: use Nix for your Python environments. uv is a great tool on a standard Linux distro, but on NixOS it's fighting the system. A short shell.nix per project is cleaner, more reproducible, and sidesteps the library problem entirely.
The approach I landed on — per-project shell files pinned to system nixpkgs via builtins.getFlake — is conventional, minimal, and decoupled from my NixOS configuration. I can add a new project by creating one file.
One less thing to maintain, it just feels more clean too.