Python Packages with Pip and Setuptools
Python Packages with Pip and Setuptools
pip install on the server is a moving PyPI. The boring default is: python3.pkgs.buildPythonApplication (CLI) or buildPythonPackage (library), deps from python3.pkgs.* on nixpkgs 26.05 — not pip in the sandbox.
poetry2nix / uv2nix exist. Use them when a lockfile is the team contract and you have measured buildPythonApplication as too coarse. They are not the first recipe.
Mental model
| Builder | For |
|---|---|
buildPythonApplication |
A CLI with project.scripts — wraps PYTHONPATH |
buildPythonPackage |
A library other Python code imports |
python3.withPackages (ps: [ ps.requests ]) |
A devShell interpreter, not a shipped package |
pyproject.toml
build-system = setuptools / hatchling / …
dependencies = python3.pkgs.requests (from nixpkgs, not PyPI at build time)
│
▼
$out/bin/desk-reporter (wrapped)
pyproject = true; on modern trees. format = "setuptools" on old setup.py only — do not set both. The host python does not matter.
| Attr (26.05) | Old gist name | Role |
|---|---|---|
build-system |
nativeBuildInputs (setuptools/hatchling) |
Builds the wheel |
dependencies |
propagatedBuildInputs |
Runtime Python deps |
nativeCheckInputs |
checkInputs |
pytest and friends |
pythonImportsCheck |
(often omitted) | import pkg after install |
pytestCheckHook in nativeCheckInputs is the nixpkgs default test runner. pythonRelaxDeps when an upstream pin is tighter than nixpkgs. Tests that hit the network fail in the sandbox — they must use python3.pkgs.
Worked examples
Case 1: Application
Save as pyproject.toml:
# pyproject.toml
[project]
name = "desk-reporter"
version = "1.0.0"
dependencies = ["requests"]
[project.scripts]
desk-reporter = "reporter:main"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"Save as desk_reporter.nix:
# desk_reporter.nix
{ lib, python3 }:
python3.pkgs.buildPythonApplication {
pname = "desk-reporter";
version = "1.0.0";
pyproject = true;
src = lib.cleanSource ./.;
build-system = [ python3.pkgs.setuptools ];
dependencies = [ python3.pkgs.requests ];
pythonImportsCheck = [ "reporter" ];
meta = {
description = "Desk metrics reporting CLI";
license = lib.licenses.mit;
mainProgram = "desk-reporter";
};
}# default.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.callPackage ./desk_reporter.nix { }nix-build default.nix
./result/bin/desk-reporter
head -5 result/bin/desk-reporterThe wrapper’s PYTHONPATH includes requests. Drop requests from dependencies and you get ModuleNotFoundError — put it back. The pyproject.toml list is documentation; nixpkgs attrs are the pin.
Case 2: Library vs application
# desklib.nix
{ python3, lib }:
python3.pkgs.buildPythonPackage {
pname = "desklib";
version = "1.0.0";
pyproject = true;
src = lib.cleanSource ./.;
build-system = [ python3.pkgs.setuptools ];
dependencies = [ python3.pkgs.pydantic ];
}Libraries do not always install a bin/. Applications do. Mixing them up gives a store path with no bin/desk-reporter.
Case 3: Tests
# tests fragment
{
nativeCheckInputs = [
python3.pkgs.pytestCheckHook
python3.pkgs.requests-mock
];
pythonImportsCheck = [ "reporter" ];
disabledTests = [
"test_talks_to_live_prometheus"
];
doCheck = true;
}pytestCheckHook is a check input, not dependencies, unless the app imports pytest at runtime (it should not). pythonImportsCheck catches a missing dependencies entry even when you skipped tests. doCheck = false is a last resort for a broken upstream suite — not a way to hide pip install in checkPhase.
pythonRelaxDeps = [ "requests" ]; when pyproject.toml says requests==2.31.0 and nixpkgs has 2.32. Prefer relaxing the pin over vendoring an old requests.
Case 4: Dev shell
# shell.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = [
(pkgs.python3.withPackages (ps: [ ps.requests ps.pytest ]))
];
}nix-shell --run "python -c 'import requests; print(requests.__version__)'"
which pythonMust be a store path. Humans run pytest here. CI builds Case 1. Do not pip install --user inside the shell “just this once.”
Case 5: Missing from nixpkgs
If desk-internal-sdk is not on 26.05:
buildPythonPackageit in this flake (callPackage ./sdk.nix).- Put it in
dependenciesof the app.
uv2nix / poetry2nix generate many package derivations from a lockfile. Worth it for an 80-dep web app. Not for a 40-line reporter. Overlaying a single buildPythonPackage is the boring missing-dep path.
The trap
The trap is pip install -r requirements.txt in buildPhase. Sandbox: no network. Even with --offline, those wheels are not Nix inputs unless you FOD them.
The other trap is propagatedBuildInputs copied from a 2019 gist. On current buildPythonPackage, use dependencies / build-system / nativeCheckInputs as in 26.05 nixpkgs python docs.
A third: format = "pyproject" and pyproject = true — pick pyproject = true. A fourth: putting pytest in dependencies so the CLI’s runtime closure includes a test runner.
The boring rule
buildPythonApplicationfor CLIs;buildPythonPackagefor libs.pyproject = true;+build-system+dependenciesfrompython3.pkgs. Notformat+pyproject.- No pip in the derivation. No unpinned
requirements.txtas the only lock. - Tests:
nativeCheckInputs(pytestCheckHook),pythonImportsCheck, hermetic. pythonRelaxDepsfor tight upstream pins. uv2nix/poetry2nix optional; one in-treebuildPythonPackagefor a missing lib.
Try this
- Case 1 with a real
pyproject.toml;nix-build;head -5 result/bin/desk-reporter(wrapper). - Drop
requestsfromdependencies; run; readModuleNotFoundError; put it back. nix path-info -Shr resultand findrequests.python3.withPackagesin a shell;python -c 'import requests'; confirmwhich pythonis a store path.- Drop
pythonImportsCheck, misspell a dependency attr, watch the build succeed and the CLI fail. PutpythonImportsCheckback.