Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started with uv2nix

Before going further and adopting uv2nix, first consider if you really want/need it. You don't need uv2nix to develop uv projects with Nix.

If you are not deploying your application with Nix it's likely that you don't want uv2nix and might be fine using the impure template from pyproject.nix:

nix flake init --template github:pyproject-nix/pyproject.nix#impure

It's also possible that you might want to develop your package(s) in an impure shell (as opposed to using editables with uv2nix, see development shell below), but deploy using uv2nix.

Any combination of approaches is possible, with different trade-offs for each.

Reading along

This getting started guide is intended to be read alongside the hello-world uv2nix template:

nix flake init --template github:pyproject-nix/uv2nix#hello-world

which contains much of the same code, but without elaboration or explanation of all the involved concepts.

Creating a pyproject.toml & uv.lock

Before anything can be done enter a development shell with required bootstrapping dependencies:

nix-shell -p python3 uv

And then use uv to create a boilerplate pyproject.toml & a uv.lock:

uv init --app --package
uv lock

You can now start adding dependencies & more.

Using pyproject.nix/uv2nix

Picking a Python interpreter

As the compatible Python versions for a workspace are encoded in the Python project files we can automatically filter nixpkgs for known compatible interpreter derivation:

python = lib.head (pyproject-nix.lib.util.filterPythonInterpreters {
  inherit (workspace) requires-python;
  inherit (pkgs) pythonInterpreters;
});

You can of course also pick a Python interpreter explicitly:

python = pkgs.python3;

Constructing a base Python set

Uv2nix uses pyproject.nix Python builders which needs to be instantiated with a nixpkgs instance:

pythonBase = pkgs.callPackage pyproject-nix.build.packages {
  inherit python;
};

Creates the necessary structure & build hooks, but it doesn't contain any Python packages itself. In uv2nix all Python packages are generated from uv.lock & explicitly added through overlays.

Loading a uv workspace

The top-level abstraction in uv2nix is the workspace, which needs to be loaded.

workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };

Will recursively discover, load & parse all necessary member projects in a uv workspace.

Uv2nix treats every project as a workspace project, even if it only contains a single pyproject.toml with a single project.

Creating a uv2nix generated overlay

Takes uv.lock & creates an overlay for use with pyproject.nix builders.

overlay = workspace.mkPyprojectOverlay {
  sourcePreference = "wheel";
};

With sourcePreference you have a choice to make:

  • wheel

Prefer downloading packages as binary wheels.

  • sdist

Prefer building packages from source.

Binary wheels are much more likely to "just work" while sdists require manual overrides more often. Wheel/sdist selection can also be done on a per-package basis.

If you are experiencing uv2nix using an sdist where you expect it to use a wheel on MacOS you might need to set the appropriate platform quirks.

Notes on build systems

uv doesn't lock build systems which are required when building packages from source. uv2nix doesn't try to hide this definiency, but instead has an overlay with the most common ones provided.

The build system overlay has the same sdist/wheel distinction as mkPyprojectOverlay:

  • pyproject-build-systems.overlays.wheel

Prefer build systems from binary wheels

  • pyproject-build-systems.overlays.sdist

Prefer build systems packages from source

Gluing everything together into a package set

Compose the Python base set + build systems + uv.lock generated packages into a concrete Python set:

pythonSet = pythonBase.overrideScope (
  lib.composeManyExtensions [
    pyproject-build-systems.overlays.wheel
    overlay
  ]
);

This set contains all Python packages as individual attributes.

Building a virtual environment

Uv2nix builds packages individually, but they aren't really useful until they're aggregated into a virtual environment.

The most convienent way to build a virtualenv is to use one of the dependency presets:

pythonSet.mkVirtualEnv "hello-world-env" workspace.deps.default

But it's also possible to specify which dependencies to install explicitly:

pythonSet.mkVirtualEnv "hello-world-env" {
  # Install hello-world with no enabled extras
  hello-world = [ ];
}

To ship package where the virtualenv is hidden see shipping applications.

Setting up a development environment (optional)

When developing Python packages local packages are normally installed in editable mode. Editable packages make entry points like scripts available in the virtual environment, but instead of installed Python files the virtualenv contains pointers to the source tree. This means that changes to the sources are immeditately activated and doesn't require a rebuild.

Uv2nix supports editable packages, but requires you to generate a separate overlay & package set for them:

editableOverlay = workspace.mkEditablePyprojectOverlay {
  # Use environment variable pointing to editable root directory
  root = "$REPO_ROOT";
  # Optional: Only enable editable for these packages
  # members = [ "hello-world" ];
};

editablePythonSet = pythonSet.overrideScope editableOverlay;

virtualenv = editablePythonSet.mkVirtualEnv "hello-world-dev-env" workspace.deps.all;

While not strictly required it's a good idea to apply source filtering to reduce how often editable packages need to be rebuilt.

The virtualenv can then be used with mkShell:

pkgs.mkShell {
  packages = [
    virtualenv
    pkgs.uv
  ];

  env = {
    UV_NO_SYNC = "1";
    UV_PYTHON = editablePythonSet.python.interpreter;
    UV_PYTHON_DOWNLOADS = "never";
  };

  shellHook = ''
    unset PYTHONPATH
    export REPO_ROOT=$(git rev-parse --show-toplevel)
  '';
}

The env attribute contains these settings:

  • UV_NO_SYNC

    Prevent uv from managing a virtual environment, this is managed by uv2nix.

  • UV_PYTHON

    Use interpreter path for all uv operations.

  • UV_PYTHON_DOWNLOADS

    Prevent uv from downloading managed Python interpreters, we use Nix instead.

The shellHook contains two interesting pieces:

Some advanced build systems (like meson-python) are more involved & require additional setup.