python-packages-extensions-overlay¶
Overlays
A nixpkgs overlay pattern for patching Python packages so the patch applies to every interpreter version in the tree — plus a clean way to expose a Python library as a top-level CLI only on machines that can build it.
The problem¶
The obvious way to override a Python package looks like this:
final: prev: {
python3Packages = prev.python3Packages.overrideScope (pyfinal: pyprev: {
foo = pyprev.foo.overridePythonAttrs (old: { doCheck = false; });
});
}
This only rewrites the scope bound to the current default interpreter. The
moment something in your tree pulls a different interpreter — a package pinned
to Python 3.11, a tool shipping its own python3.env, a cross build, or simply
python312Packages.foo when the default is 3.13 — it gets the unpatched
foo back.
The worst part is that this fails silently. The build succeeds; it just uses the version you thought you had fixed. You disabled a flaky test, or added a missing dependency, and it quietly didn't take.
The insight¶
nixpkgs threads a list called pythonPackagesExtensions into the construction
of all interpreter package sets. Append your override to that list and it is
applied once, everywhere:
final: prev: {
pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [
(pyfinal: pyprev: {
foo = pyprev.foo.overridePythonAttrs (old: { doCheck = false; });
})
];
}
Always ++ (append), never assign — other overlays contribute extensions too,
and replacing the list drops theirs.
Layout¶
default.nix— the aggregator overlay. It imports each override module, applies it to the top-levelprev, and appends the results topythonPackagesExtensions. It also shows the conditionaltoPythonApplicationre-wrap (below).python-modules/*.nix— one small file per override. Keeping them separate makes each patch self-documenting and easy to remove when nixpkgs catches up.packages/*.nix— vendored derivationscallPackaged by a module, for packages the pinned nixpkgs does not ship at all.
Module signature¶
Each module file is a function of the top-level prev, returning an ordinary
python-package-set extension:
topPrev: pyfinal: pyprev: { <pkg> = ...; }
# │ │ └─ previous python package set (a.k.a. super)
# │ └────────── final python package set (a.k.a. self)
# └─────────────────── top-level `prev` pkgs set (for lib, fetchers, config, …)
- Patch an existing package with
pyprev.<pkg>.overridePythonAttrs. - Add a new package with
pyfinal.callPackage. - Reach for
pyfinal(self) when a value you inject may itself be patched by a later extension in the list; usepyprev(super) for the thing you are overriding.
The included modules demonstrate the three recurring cases:
| module | case |
|---|---|
disable-sandbox-tests.nix |
drop tests that only fail in the Nix build sandbox |
add-missing-dependency.nix |
inject a dep the pinned nixpkgs omits |
vendored-package.nix |
build a package nixpkgs doesn't ship (yet) |
They use placeholder package names (example-*) — replace them with yours.
vendored-package.nix callPackages ../packages/example-vendored.nix, which
does ship with the recipe as a worked example: it is an ordinary
buildPythonPackage whose src hash is lib.fakeHash, so the attribute
evaluates cleanly and the first build prints the real hash to paste in. Replace
its pname/version/src/inputs with your package (or delete the module).
Conditional library-to-application re-wrap¶
Some Python packages are only worth running as a CLI on hosts with the right
build support (GPU/accelerator, large toolchains). default.nix shows how to
expose the library as a top-level application only where it can build:
example-accel-tool =
if prev.config.cudaSupport or false then
final.python3Packages.toPythonApplication final.python3Packages.example-accel-tool
else
prev.example-accel-tool or null;
toPythonApplicationtakes the (already-patched) library derivation and exposes it as a runnable top-level program.- Pull it from
final.python3Packagesso it inherits your extensions. - On hosts without the gate, pass the upstream attribute through, falling back
to
nullwithorso evaluation never throws where the attr is undefined.
Swap cudaSupport and example-accel-tool for your own gate and package.
Gotchas¶
- Append, don't replace
pythonPackagesExtensions. doCheck = falseis sometimes not enough. Several nixpkgs Python packages moved their pytest run intonativeInstallCheckInputs, so it runs in the installCheck phase. SetdoInstallCheck = falsetoo when a test still runs after you disableddoCheck.- The extension runs for every interpreter — make sure the package actually
exists in each set, or guard with
pyprev ? <pkg>if it may not.
Usage¶
# NixOS
nixpkgs.overlays = [ (import ./python-packages-extensions-overlay) ];
# or standalone
import nixpkgs {
overlays = [ (import ./python-packages-extensions-overlay) ];
}
Source¶
overlays/python-packages-extensions-overlay/default.nix
# python-packages-extensions-overlay
#
# Route Python package overrides through `pythonPackagesExtensions` instead of
# a flat `python3Packages` override, so every patch stacks across *every*
# interpreter version present in the tree (python311Packages, python312Packages,
# python313Packages, …) rather than silently touching only the one you named.
#
# A flat `python3Packages.overrideScope (…)` only rewrites the scope bound to the
# current default interpreter. Anything in the tree that pulls a *different*
# interpreter — a package pinned to 3.11, a tool that ships its own python3.env,
# a cross build — gets the unpatched package back, and the failure is silent:
# the build succeeds, it just uses the version you thought you had fixed.
# `pythonPackagesExtensions` is the list nixpkgs threads into the construction of
# *all* interpreter package sets, so appending to it applies your override once
# and everywhere.
#
# This is a standard nixpkgs overlay: `final: prev:`. Add it to
# `nixpkgs.overlays` (NixOS) or `import nixpkgs { overlays = [ ... ]; }`.
final: prev:
let
# Each member is a function that receives the top-level `prev` (so a module can
# reach `prev.lib`, `prev.fetchFromGitHub`, `prev.config`, etc.) and returns a
# python-package-set extension of the usual `pyfinal: pyprev: { … }` shape.
#
# The doubly-nested signature that shows up in the module files below is:
#
# topPrev: pyfinal: pyprev: { <pkg> = ...; }
# \_____/ \_____/ \_____/
# │ │ └─ the previous python package set (a.k.a. `super`)
# │ └────────── the final python package set (a.k.a. `self`)
# └─────────────────── the top-level `prev` pkgs set
#
# Use `pyprev.<pkg>.overridePythonAttrs` to patch an existing package, or
# `pyfinal.callPackage` to add a new one. Prefer `pyfinal` when you want a
# value that itself may have been patched by a later extension in the list.
modules = map (f: f prev) [
(import ./python-modules/disable-sandbox-tests.nix)
(import ./python-modules/add-missing-dependency.nix)
(import ./python-modules/vendored-package.nix)
];
in
{
# Append (never replace) so extensions contributed by other overlays survive.
pythonPackagesExtensions = prev.pythonPackagesExtensions ++ modules;
# Conditionally re-wrap a *library* as a top-level *application*.
#
# Some packages are only usable as a CLI on machines that have the right
# accelerator/build support (here: CUDA). `toPythonApplication` takes the
# library derivation from the (already-patched) python package set and exposes
# it as a top-level runnable program — but only where it makes sense to build.
# Elsewhere we pass the upstream attribute through unchanged, falling back to
# `null` if nixpkgs doesn't define it at all, so evaluation never throws.
#
# Swap `example-accel-tool` / `cudaSupport` for your own package and gate.
example-accel-tool =
if prev.config.cudaSupport or false then
final.python3Packages.toPythonApplication final.python3Packages.example-accel-tool
else
prev.example-accel-tool or null;
}