Why I Switched from uv to nix-shell for Python on NixOS

Per-project Nix environments instead of uv virtualenvs

3 min read

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.nix in ecommerce-sales-profit-analysis, us-consumer-finance-complaints, and noahs-rug
  • Removed uv from my NixOS home.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-shell reads shell.nix; nix develop requires flake.nix. These aren't interchangeable.
  • import <nixpkgs> looks unpinned — versions drift with nix-channel --update. Using builtins.getFlake avoids this entirely.
  • nixpkgs-fmt formats 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.
  • black and jupytext stayed 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.