Python

The Python rules model applications and pytest suites backed by a locked pyproject.toml project. Applications build into self-contained PEX files; tests create an isolated uv environment and run pytest. Ruff supplies the format and lint products for application targets.

Set up the workspace

Import the Python rules in imp.workspace.js; they provide default uv, PEX, and CPython toolchains. Add Ruff when Python targets should participate in formatting and linting:

import "//rules/python";
import "//rules/python/ruff_toolchain";
import "//rules/workflows/fmt";
import "//rules/workflows/lint";
import "//rules/workflows/package";
import "//rules/workflows/test";

Projects are built with uv sync --locked. Keep uv.lock checked in and in sync with pyproject.toml; builds and tests fail rather than resolving or mutating a stale lock file.

Select a workspace lockfile

The rules ship a lockfile for each managed tool — uv, PEX and Ruff. It pins the download URL, size, and SHA-256 of every release artifact, and it knows only the versions the rules ship with. To pin a version that the shipped lockfile does not know, give the toolchain the address of a lockfile that this workspace owns. The shipped lockfiles stay the default.

export const uvDefault = uvToolchain("0.11.17", {
	default: true,
	lockfile: "//locks/uv.lock",
});
export const pexDefault = pexToolchain("2.98.0", {
	default: true,
	lockfile: "//locks/pex.lock",
});
export const ruffDefault = ruffToolchain("0.15.22", {
	default: true,
	lockfile: "//locks/ruff.lock",
});

Export the lockfile generation roots, then write the three files:

import { pexGenLockfiles, uvGenLockfiles } from "//rules/python";
import { ruffGenLockfiles } from "//rules/python/ruff_toolchain";

export const uvLockfiles = uvGenLockfiles();
export const pexLockfiles = pexGenLockfiles();
export const ruffLockfiles = ruffGenLockfiles();
imp goal gen-lockfiles //:uvLockfiles //:pexLockfiles //:ruffLockfiles

Each generation root writes to the address that its toolchain declares, so the address is given one time only. Downloads stay verified: an address with no file, or a lockfile with no entry for the selected version and platform, makes the acquire fail and points at imp goal gen-lockfiles.

Select a dependency resolve

Declare a pythonResolve() for a locked project when applications or tests need optional dependency flavors. A resolve uses uv's checked-in uv.lock as the source of truth; each flavor selects one project extra at sync time.

import { pythonResolve } from "//rules/python";

export const ml = pythonResolve({
    path: "services/ml",
    flavors: {
        default: { extra: "cpu" },
        cpu: { extra: "cpu" },
        cu124: { extra: "cu124" },
    },
});

Then pass it to targets; the resolve supplies their project path (so do not also set src):

export const app = pythonApp({ resolve: ml, entryPoint: "acme.__main__" });
export const tests = pythonTest({ resolve: ml });

Choose a flavor with --axis python=cu124. A workspace can also define a named --profile containing python: "cu124". The resolve's pyproject.toml must declare the matching uv extra and use uv's explicit indexes/sources to bind PyTorch packages to the CPU or CUDA index. Flavors are deliberately extras rather than package-version syntax: uv locks the real PyTorch local version (for example +cu124) and PEX packages the resulting synced venv.

// imp.workspace.js
import { defineProfile } from "imp:core";
defineProfile("cu124", { python: "cu124" });

Declare an application and tests

import { pythonApp, pythonTest } from "//rules/python";

export const app = pythonApp({
    entryPoint: "acme.__main__",
});

export const tests = pythonTest({
    testArgs: ["-q"],
});

Both targets default to the directory containing BUILD.js; set src to point at another project directory. pythonApp syncs the locked environment and asks PEX to package the project and its installed dependencies. entryPoint becomes the PEX entry point, while extraPexArgs is available for PEX options that do not yet have a dedicated field.

pythonTest is intentionally separate from pythonApp: it does not build a PEX first, and its testArgs are appended to python -m pytest. Successful test tasks are reused when their inputs match; failures produce no cached result and run again next time.

Run source files

For direct script execution, declare one shallow source set rather than an application target per file. It uses the Python rule's pinned runtime:

import { pythonSources } from "//rules/python";

// BUILD.js
export const scripts = pythonSources({
    root: "tools",
    sources: ["*.py"],
});

Each direct match becomes one selectable child of the exported source set, keyed by its workspace-relative path. Run it by file path, including arguments after --:

imp run tools/demo.py -- --verbose

A bare path resolves to whichever exported source set claims that file, searching the file's own package first and then each parent package, so the scripts export above does not have to be named. When two source sets glob the same file, the path is ambiguous and imp says so; select one explicitly by its full child address instead:

imp run //tools:scripts#tools/demo.py -- --verbose

The source set's complete file list is staged and root is added to PYTHONPATH. The run goal keeps the process sandboxed but sets its working directory to the real workspace, so files written by the script persist there. Nested directories need their own pythonSources() declaration; recursive ** patterns are rejected.

Sources do not require a project. They run with the pinned interpreter and the standard library by default. A workspace may declare one optional locked project to supply third-party dependencies:

import { pythonResolve } from "//rules/python";

export const project = pythonResolve({ path: "python" });

Pass that resolve to a source set when it needs third-party dependencies:

export const scripts = pythonSources({
    root: "tools",
    sources: ["*.py"],
    resolve: project,
});

The resolve is synchronized from its checked-in pyproject.toml and uv.lock without becoming the source owner. pythonProject({ default: true }) remains as a compatibility alias for the previous single-default-project source-run API. Import/dependency inference and multiple project resolutions are intentionally future work.

Run goals and find outputs

imp build //services/acme:app
imp test //services/acme:tests
imp fmt --check //services/acme:app
imp lint //services/acme:app
imp package //services/acme:app

build captures the generated .pex as a build artifact without writing it into the source tree. package publishes it below dist/services/acme/app. Formatting and linting are graph facets attached at construction time by importing the Ruff extensions; select the application target when checking the project's Python files.

Configuration

pexToolchain()

pexToolchain(version, opts = {})

Declare a pex toolchain version and optionally set it as the default. defaults to the bundled //rules/python/pex-toolchain.lock. Point this at your own lock (regenerate via imp goal gen-lockfiles) when pinning a version the bundled lock does not know. matching lockfile entry (warns instead of failing).

ParameterTypeDescription
versionstring
[opts]object
[opts.default=false]boolean
[opts.lockfile]stringLockfile address pinning download SHA-256s;
[opts.unverified=false]booleanAllow downloading without a

Returns: object Target handle for this pex toolchain.

ruffToolchain()

ruffToolchain(version, opts = {})

Declare a ruff toolchain version and optionally set it as the default. defaults to the bundled //rules/python/ruff-toolchain.lock. Point this at your own lock (regenerate via imp goal gen-lockfiles) when pinning a non-default version. matching lockfile entry (warns instead of failing).

ParameterTypeDescription
versionstring
[opts]object
[opts.default=false]boolean
[opts.lockfile]stringLockfile address pinning download SHA-256s;
[opts.unverified=false]booleanAllow downloading without a

Returns: object Target handle for this ruff toolchain.

uvToolchain()

uvToolchain(version, opts = {})

Declare a uv toolchain version and optionally set it as the default. defaults to the bundled //rules/python/uv-toolchain.lock. Point this at your own lock (regenerate via imp goal gen-lockfiles) when pinning a version the bundled lock does not know. matching lockfile entry (warns instead of failing).

ParameterTypeDescription
versionstring
[opts]object
[opts.default=false]boolean
[opts.lockfile]stringLockfile address pinning download SHA-256s;
[opts.unverified=false]booleanAllow downloading without a

Returns: object Target handle for this uv toolchain.