pytest-notebook-policy is a pytest plugin that enforces notebook policy and quality rules.
It focuses on notebook-specific checks for marimo and Jupyter workflows, not generic Python linting.
If you are looking for notebook best practices, assurance, validation, or testing tooling, this project is intended to cover those needs through enforceable policy checks and automated quality gates.
pytest-notebook-policy is a lightweight semantic checker for notebook workflows.
It focuses on enforcing notebook patterns that are easy to miss in review, such as:
on_changecallback usage where reactivity is clearer- cross-cell mutation of shared objects
- non-idempotent cell behaviour
- mixed test/helper cells and fixture placement conventions
marimo already gives you:
- native notebook testing with
pytest - built-in notebook linting via
marimo check(announcement)
pytest-notebook-policy is designed to complement those tools with opinionated, team-level checks tailored to a stricter “production notebook” style.
In practice:
- use Ruff for general Python quality/security
- use marimo check for core notebook validity and formatting rules
- use pytest-notebook-policy for extra policy checks around reactive design and notebook maintainability
pytest-notebook-policy is especially useful as an automated quality gate when notebooks are generated or edited by coding agents (for example Claude, Warp, Codex, or similar tools).
Adding it to pre-commit and CI helps catch marimo-specific issues immediately, so agents can self-correct before code reaches review.
Example pre-commit hook:
repos:
- repo: local
hooks:
- id: pytest-notebook-quality
name: pytest-notebook-quality
entry: uv run pytest-notebook-quality experiments notebooks
language: system
pass_filenames: falseThis keeps the feedback loop short:
- agent proposes notebook edits
- pre-commit/CI runs Ruff +
pytest-notebook-policychecks - agent fixes violations and retries
M001: prefer reactive dependencies overon_changehandlers.M002: keep test cells focused; avoid mixing tests with helper/setup code in the same cell.M003: avoid mutable module-level state in notebook files.M004: prefer fixtures inconftest.pyor helper modules rather than notebook modules.M005: avoid cross-cell mutation of shared objects (including notebook inputs and module-level mutable state).M006: avoid non-idempotent calls in cells (for examplerandom.*,np.random.*,time.time,uuid.uuid4).J001: avoid notebook magics and shell escapes in policy-checked notebooks.J002: avoid non-idempotent calls in Jupyter notebook code cells.J010: (opt-in) check that paired.ipynband.pyfiles stay in sync.J011: require a top-of-notebook parameter/configuration cell in the first few code cells.J012: keep notebooks and cells small enough to stay reviewable and maintainable.J013: avoid excessive inline function/class definitions in notebooks; extract reusable logic into modules. Detailed rationale and remediation guidance:docs/RULES.md.
Install in a project:
uv add --dev pytest-notebook-policyInstall pre-commit hooks:
uv run --with pre-commit pre-commit installRun hooks across all files:
uv run --with pre-commit pre-commit run --all-filesCI runs on push/PR using .github/workflows/ci.yml and executes Ruff plus the test suite.
Run checks explicitly:
uv run pytest --notebook-checkEnable by default in pyproject.toml:
[tool.pytest.ini_options]
notebook_check = trueFilter rules:
uv run pytest --notebook-check --notebook-check-select M001 --notebook-check-ignore M004Choose Jupyter rule source:
uv run pytest --notebook-check --notebook-check-jupyter-source paired-pySet default Jupyter rule source in pyproject.toml:
[tool.pytest.ini_options]
notebook_check = true
notebook_check_jupyter_source = "paired-py"paired-py prefers the paired .py notebook (when available and readable) for J-rules and falls back to .ipynb.
Tune Jupyter size/complexity thresholds:
uv run pytest --notebook-check \
--notebook-check-jupyter-max-code-cells 30 \
--notebook-check-jupyter-max-cell-lines 120 \
--notebook-check-jupyter-max-inline-definitions 5Set defaults in pyproject.toml:
[tool.pytest.ini_options]
notebook_check_jupyter_max_code_cells = "30"
notebook_check_jupyter_max_cell_lines = "120"
notebook_check_jupyter_max_inline_definitions = "5"Run combined Ruff + notebook policy checks:
uv run pytest-notebook-quality path/to/notebooksSkip Ruff and run only notebook policy checks:
uv run pytest-notebook-quality --skip-ruff path/to/notebooksCustomise deterministic rule toggles and thresholds on the quality command:
uv run pytest-notebook-quality --skip-ruff \
--notebook-check-select M \
--notebook-check-select J \
--notebook-check-ignore J010 \
--notebook-check-jupyter-source paired-py \
--notebook-check-jupyter-max-code-cells 30 \
--notebook-check-jupyter-max-cell-lines 120 \
--notebook-check-jupyter-max-inline-definitions 5 \
path/to/notebooksWrite an NBOM-style JSON manifest (notebook surface + dependency correlation):
uv run pytest-notebook-quality --skip-ruff \
--report-nbom-json reports/notebook-policy-nbom.json \
--report-dependency-enrichment \
path/to/notebooksInclude optional vulnerability IDs (queried from OSV) in dependency enrichment output:
uv run pytest-notebook-quality --skip-ruff \
--report-nbom-json reports/notebook-policy-nbom.json \
--report-dependency-vulns \
path/to/notebooks--report-dependency-vulns implicitly enables dependency enrichment.
Generate a markdown report with findings-first layout, touchpoint summary, and appendices:
uv run pytest-notebook-quality --skip-ruff --report-md reports/notebook-policy-report.md path/to/notebooksEnable optional dependency enrichment in the report:
uv run pytest-notebook-quality --skip-ruff \
--report-md reports/notebook-policy-report.md \
--report-dependency-enrichment \
path/to/notebooksProject-specific quality defaults can be set in pyproject.toml:
[tool.pytest_notebook_policy.quality]
select = ["M", "J"]
ignore = ["J010"]
jupyter_source = "paired-py"
jupyter_max_code_cells = 30
jupyter_max_cell_lines = 120
jupyter_max_inline_definitions = 5
report_md = "reports/notebook-policy-report.md"
report_dependency_enrichment = true
report_dependency_vulns = false
report_nbom_json = "reports/notebook-policy-nbom.json"Enable optional sync tooling:
uv add --dev 'pytest-notebook-policy[sync]'- Versioning follows Semantic Versioning (
MAJOR.MINOR.PATCH). - Release history lives in
RELEASE_NOTES.md.
Typical release flow:
uv version --bump patch
uv build
uv run --with twine twine check dist/*When ready to release (not run yet here), upload with Twine:
uv run --with twine twine upload dist/*The repository includes notebook fixtures in tests/fixtures:
tests/fixtures/synthetic: synthetic notebooks for targeted pass/fail checks.tests/fixtures/real: real-world notebooks sourced from public repositories (with provenance intests/fixtures/real/SOURCES.txt).
Refresh pinned real fixtures and print their observed rule-code sets:
uv run python scripts/refresh_real_fixtures.py