PolicyEngine model development
Engineering patterns for the country-model repos — policyengine-us, policyengine-uk,
policyengine-canada. These packages encode enacted law (and proposed reforms) as variables
(Python formulas) driven by parameters (YAML), validated by YAML tests, all running on
the vectorized policyengine-core engine.
This skill is the how-to-write-it layer. To run calculations or score reforms, use the
policyengine skill instead. Verified against policyengine-us 1.764.x / policyengine-core 3.30.x
(2026-07).
For delegated implementation, test authoring or review, follow agent-loading.md: every worker loads this entrypoint in its own context, reads its task references and reports evidence of the load before work.
Mental model
- Parameters are a YAML tree under
parameters/gov/.... Each leaf is a dated value (or a bracket/breakdown table) with required metadata. Accessed in formulas asparameters(period).gov.agency.program.thing. Federal undergov/{agency}/, state undergov/states/{st}/, contributed reforms undergov/contrib/. - Variables live one-per-file under
variables/gov/..., each aVariablesubclass withvalue_type/entity/definition_period/ metadata and either aformulaor anadds/subtractslist (never both). Formulas run vectorized over the whole population. - Entities nest:
Person→TaxUnit/SPMUnit/Family/MaritalUnit/Household(US);Person→BenUnit/Household(UK). Aggregation across entity levels is automatic viaadds/add(). - Tests are YAML files mirroring the variable path under
tests/policy/baseline/..., asserting outputs for a described household at a period.
The absolute musts
- Never hardcode a numeric policy value in a formula. Every threshold, rate, and amount
comes from a parameter (
p.income_limit, not2000). Bare0/1/-1andMONTHS_IN_YEARare the only acceptable literals. See references/variables.md. - Vectorize everything. No
if/elif/elseorand/or/noton entity arrays — usewhere/select/&/|/~. Pythonifis allowed only on scalar parameters (if p.flat_applies:). See references/vectorization.md. adds/subtractsXORformula— never both in one variable. A pure sum uses theaddsattribute with no formula; anything else uses a formula (withadd()inside). See references/periods-and-aggregation.md.- Get the period right. From a MONTH formula, YEAR flow variables (income) use
period(auto ÷12); YEAR stocks/counts/ages/booleans useperiod.this_year(no division). See references/periods-and-aggregation.md. uv runfor everything.uv run pytest ...,uv run policyengine-core test .... Never barepytest. Format withuv run ruff(line length 88, the default — not 79). There is noblackin this toolchain.- Changelog = a towncrier fragment, never an edit to
CHANGELOG.md. Writechangelog.d/{branch-name}.{added|fixed|changed}.mdwith one line. - Branch from the PolicyEngine upstream repo, not a fork. Encoding work targets
origin/mainofPolicyEngine/policyengine-us(etc.).
Routing
| You are working on… | Read |
|---|---|
| A variable: value_type/entity/defined_for, adds vs formula, naming, gotchas (age float, monthly_age, is_ssi_eligible), federal aggregators | references/variables.md |
| A parameter: YAML structure, brackets (-.inf, .inf, single_amount, right=True boundary conventions), breakdowns, metadata, path syntax | references/parameters.md |
| Periods (period vs period.this_year, ÷12) or aggregation (adds/add(), .any()) | references/periods-and-aggregation.md |
| Vectorization: where/select, max_(A-B,0) floors, divide-by-zero, phantom values | references/vectorization.md |
| YAML tests: structure, period restrictions, error margins, enums, the CLI | references/tests.md |
| A contributed reform under contrib/ / reforms/: factory, in_effect, registration | references/reforms.md |
| Code style, framework constants, folder layout, review response | references/style.md |
Setup
cd policyengine-us # or -uk / -canada; branch off origin/main
uv pip install -e ".[dev]" # editable install with dev deps
uv run pytest policyengine_us/tests/... -q
uv run policyengine-core test <yaml_path> -c policyengine_us # runs a YAML test file
uv run ruff format . && uv run ruff check .
Discover names against the live tree instead of guessing — grep the variables/ and
parameters/ directories, or introspect the parameter tree in Python (see
references/parameters.md). Paths and parameter names change; the repo is the source of truth.