Shared contribution guide for all PolicyEngine repos. Individual repos may add a per-repo CONTRIBUTING.md with commands and conventions specific to that codebase — it takes precedence over this file when present.
If you're reading this as an AI coding agent, start here. Commands are copy-pasteable and the anti-patterns section is explicit.
- Find or file an issue describing the bug or feature.
- Branch from the default branch on the upstream repo (not a fork, not local). Most repos use
main; a few still usemaster— checkgh repo view PolicyEngine/<repo> --json defaultBranchRef --jq .defaultBranchRef.name. - Write code + tests. Bug fixes need a test that fails without the fix. New public APIs need tests that cover them.
- Add a changelog fragment (see below).
- Run the repo's format target (
make formatin almost every repo). CI rejects unformatted code. - Open a PR with
Fixes #<issue>and a layperson-readable explanation of why. - Wait for all CI jobs to pass before merging. Never merge red CI.
PolicyEngine Python repos use uv for dependency management. Always use uv run for Python commands:
uv run pytest tests/path/to/test.py -v
uv run python scripts/something.pyDo not use bare python / pytest / pip — they may pick up the wrong environment and cause CI mismatches.
Install a repo with make install (wraps uv sync and dev-extras).
Short, action-oriented, no agent-label prefix. Good: fix-uc-childcare-variable-hours, add-scottish-carer-payment. Bad: codex/fix-123, claude/whatever, master-2.
Every PolicyEngine Python repo uses towncrier fragments under changelog.d/. On merge to the default branch, a Versioning workflow runs towncrier build to compile the fragments into CHANGELOG.md and bump the version automatically.
To add an entry:
echo "One-sentence description in past tense." \
> changelog.d/<your-branch-name>.<type>.mdFragment <type> drives the SemVer bump:
| Type | Bump | Use when |
|---|---|---|
added |
minor | A new public API, variable, parameter, or program. |
changed |
patch | An existing feature's behaviour has changed. |
fixed |
patch | A bug fix or improvement to an existing calculation. |
removed |
minor | A public API, variable, parameter, or program is gone. |
breaking |
major | A change downstream consumers will need to adapt to. |
- First line:
Fixes #<issue>(orCloses #<issue>) so merging the PR closes the tracked issue. - Body: layperson-readable explanation of why (motivation) and what (summary of changes). Include a test-plan checklist for non-trivial PRs.
- Breaking changes: a separate "Migration" section with explicit before/after guidance.
All PRs must be reviewed by someone other than the author. If no other reviewer is available, wait at least 24 hours from your last commit before self-review. Reviewers should look for tests, changelog fragment presence, and CI green status.
Things that reliably break CI or cause silent data loss — don't do any of these:
- Don't edit
CHANGELOG.mddirectly. It is overwritten by thetowncrier buildthat runs on merge. - Don't create
changelog_entry.yaml. The YAML-changelog format is deprecated across every PolicyEngine repo; the PR CI'sCheck changelog fragmentstep rejects it. - Don't run
make changelogmanually during PR creation. The workflow does this on merge. - Don't push to the default branch. Always open a PR.
- Don't open PRs from forks if the repo has workflow secrets. Fork PRs can't access
secrets.*and their CI will fail on data-download or GitHub-API steps. Create the branch on the upstream repo instead. - Don't create new files with
_v2/_v3/_newsuffixes. Edit the original in place — git keeps the history. - Don't delete code without grepping for callers. Passing tests ≠ safe to delete (tests may bypass the code being removed).
- Don't hardcode values to make a specific test pass. Fix the root cause.
- Don't publish a number the model didn't compute. Every fiscal, distributional, poverty or household figure presented as a PolicyEngine estimate in an analysis, dashboard or report comes from running the microsimulation for that scenario, never from scaling, interpolating or extrapolating other runs' outputs, or from a side model standing in for PolicyEngine. A macro or price scenario goes in as model inputs, so the model's uprating carries it through. If there are too many scenarios to run, run fewer and say how many runs stand behind each figure. Agent-facing details are in the
policyengineskill in policyengine-skills. - Don't use
# pragma: no coverfor code that simply lacks tests — write tests instead.
Use # pragma: no cover only where code cannot be exercised in a unit test:
- Microsimulation-specific branches:
simulation.is_over_dataset,simulation.has_axes. - Behavioural-response branching:
simulation.get_branch(),simulation.baselineaccess.
Everything else should be covered by a test.
Country repos (policyengine-uk, policyengine-us, etc.) carry their own conventions around:
- variable / parameter directory structure (
variables/gov/<agency>/...,parameters/gov/<agency>/...) - period handling, entity structure, formula idioms (
where,max_,min_, not Pythonif/max/min) - regional/categorical breakdowns (filing_status, region, tenure_type)
- regulatory citation requirements in
referencefields
See the per-repo CONTRIBUTING.md and CLAUDE.md for specifics.
- British English, sentence case for PolicyEngine blog posts, docs, and UI copy.
- Comments describe current behaviour, not history ("computes X" not "previously computed Y, now computes X" — git log is the history).
- PR titles under 70 characters; use the body for detail.