Python
mkSecureArtifact detects a Python project by a pyproject.toml at the root of
src. It resolves dependencies from a Poetry lock file or a uv lock file (see
uv projects), builds the application, and hands the result to
mkHardenedImage.
The marker
The primary marker is pyproject.toml. A poetry.lock or uv.lock alongside
it is the strongest signal for a hermetic build and is recorded as the
buildConfigFile.
Required and optional ecosystemArgs
lock(required) — a vendored relative path to the Poetry lock file, e.g../poetry.lock. It must be a path in your repository, not a read out of the fetchedsrc. Reading a lock file out of a fetched derivation is import-from-derivation, whichnix flake check --no-buildcannot evaluate and the repo’sno-ifd-lockfilelint forbids.python(optional) — the interpreter (defaultpython312).overrides(optional) —{ "package-name" = derivation; }to replace a generated dependency entirely. Use it for packages with native (maturin / C) build steps, non-PyPI sources, or private sources.exclude(optional) — a list of exact lock names to skip (Windows-only, wheel-only-with-no-linux-source, or intentionally omitted packages).format(optional) — the Python build format (default"pyproject").
Worked example
Vendor the lock file into your repo first, then:
hb.mkSecureArtifact {
name = "my-api";
version = "2.0.1";
src = ./.;
ecosystemArgs = {
lock = ./poetry.lock; # vendored, relative
python = pkgs.python312;
exclude = [ "pywin32" "pyroscope-io" ];
overrides = {
"some-rust-pkg" = mySomeRustPkgDrv;
};
};
image.cmd = [ "/bin/my-api" ];
}
Under the hood, the emitter reads poetry.lock at eval time with
builtins.fromTOML (Poetry locks carry per-package sha256 hashes, so no
build-time conversion and no IFD), builds every dependency that nixpkgs does not
already provide via buildPoetryDeps, and wires them in as
propagatedBuildInputs. Packages already present in python.pkgs are reused,
not rebuilt.
Generated dependency derivations skip their upstream test suites
(doCheck = false) — those virtually always need network, live services, or
dev-only deps not in the lock. If you need an import check on a specific
package, supply it through overrides.
uv projects
When lock points at a uv.lock, the emitter builds the project with
uv2nix instead of buildPoetryDeps.
uv2nix reads uv.lock at eval time with builtins.fromTOML (no IFD), and
build-system-pkgs supplies
the build backends (setuptools, flit-core, hatchling, …) that uv.lock doesn’t
record per package.
Unlike the Poetry path, every package comes from the lock at its locked version,
including packages nixpkgs also carries, as the Go, Rust and Ruby emitters do.
The result is a virtual environment with the project and its default
dependencies installed (no optional extras), so the project’s console scripts
end up in its bin/.
By default every package in uv.lock is built from its source distribution, and
a dependency that only publishes wheels fails the evaluation. uv.lock doesn’t
record build backends, so each source-built dependency needs its backend added
through overrides (or tool.uv.extra-build-dependencies). The backends
themselves come from build-system-pkgs, which installs uv-build (the
uv_build backend uv init sets up) from a prebuilt PyPI wheel. It’s only used
during the build and doesn’t end up in the image. Dependencies from git
sources, and wheels without a hash, aren’t supported, since uv2nix fetches them
without a hash check. Any such entry anywhere in uv.lock fails the evaluation,
including ones in dev groups or for other platforms.
lock(required) — a vendored relative path touv.lock, e.g../uv.lock, with a copy of the project’spyproject.tomlnext to it (and of each workspace member’s, at the same relative path). uv2nix reads them at eval time; the project itself is built fromsrc.python(optional) — the interpreter (defaultpython312). It must satisfy the project’srequires-python.sourcePreference(optional) —"sdist"(default) or"wheel"."wheel"installs the locked, hash-pinned PyPI wheels where they exist, so dependencies don’t need their build backends added. AGENTS.md prefers building from source, so state the reason next to it in the project’s flake. Compiled wheels can bundle native libraries (e.g. OpenSSL in cryptography’s) that the SBOM’s closure walk doesn’t list.overrides(optional) —{ "package-name" = derivation; }to replace a package, or an overlayfinal: prev: { ... }to patch one, e.g. to point a package at a nixpkgs library.
format and exclude don’t apply to uv projects: the build format comes from
each package’s metadata, and uv2nix evaluates each dependency’s environment
markers, so platform-specific packages (e.g. Windows-only) are skipped on their
own.
hb.mkSecureArtifact {
name = "my-api";
version = "2.0.1";
src = ./.;
ecosystemArgs = {
lock = ./uv.lock; # vendored, relative
python = pkgs.python313;
overrides = final: prev: {
some-pkg = prev.some-pkg.overrideAttrs (old: {
buildInputs = (old.buildInputs or [ ]) ++ [ pkgs.zlib ];
});
};
};
image.cmd = [ "/bin/my-api" ];
}
Derivation-first alternative
If your build doesn’t fit the Poetry-lock shape, build the application yourself and image it directly:
let
myApi = pkgs.python312.pkgs.buildPythonApplication {
pname = "my-api";
version = "2.0.1";
src = ./.;
format = "pyproject";
propagatedBuildInputs = with pkgs.python312.pkgs; [ fastapi uvicorn ];
};
in
hb.mkHardenedImage {
name = "my-api";
version = "2.0.1";
package = myApi;
cmd = [ "/bin/my-api" ];
}
SBOM
Python wheels/sdists in the closure are captured in the CycloneDX SBOM. See SBOMs for how ecosystem dependencies are folded in and how to override auto-detection when needed.