Skip to content

Stop charging property stocks as purchases when a household buys its home - #2241

Merged
MaxGhenis merged 6 commits into
mainfrom
fix-additional-dwelling-purchase-2238
Oct 15, 2026
Merged

MaxGhenis merged 6 commits into
mainfrom
fix-additional-dwelling-purchase-2238

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2238.

What was wrong

additional_residential_property_purchased was other_residential_property_value × property_purchased. property_purchased is the dataset's purchase flag: a seeded draw on 3.85% of households in the data build (3.3% weighted), set to match all UK housing transactions. So every household that bought a home had its whole stock of other residential property charged as a single additional-dwelling purchase at the higher rates. The variable's own documentation says "Only include the value of a single purchase."

non_residential_property_purchased (defined_for = "property_purchased", adds = ["non_residential_property_value"]) did the same with the non-residential stock, so I fixed it here too.

The issue's reproducer: a £600,000 main residence, £2,000,000 of other residential property, 2025. Buying the home cost £233,750 of SDLT, of which £213,750 was higher-rates SDLT on the £2m stock.

The law

SDLT is charged on the chargeable consideration for a land transaction (Finance Act 2003 s. 55). The higher rates for additional dwellings (Schedule 4ZA) apply to a purchase of a dwelling by someone who will own another dwelling and is not replacing their main residence. A stock of property already owned is not a transaction, and buying a main residence does not purchase that stock.

Changes

  • property_purchased now means "bought its main residence this year". It sets only main_residential_property_purchased, as before.
  • additional_residential_property_purchased (the price of one additional dwelling) and non_residential_property_purchased become inputs, nil unless set directly or by a dataset that imputes the purchases. They are listed with the property stocks in data/uprating_indices.yaml, so imputed prices are uprated the same way.
  • SDLT, LBTT and LTT formulas are unchanged: they already read these purchase prices.

This follows the replay's own construction rule (scorecard#157, build_uk_event_registry.py): "The certified population must contain additional-home purchase values; absence yields an inert leg rather than an inferred national tax base."

Tests

Examples (YAML). The issue's three cases:

  • No purchase: no SDLT.
  • A main-residence purchase with £2m of other residential and £0.5m of non-residential property: SDLT is only the main-rates £20,000.
  • An explicit £400,000 additional-dwelling purchase in 2022: the higher-rates tax on £400,000, on top of the main-residence tax.

Properties (test_property_transaction_tax_purchase_properties.py, Hypothesis, households across England, Northern Ireland, Scotland and Wales, 2022-2027). Invariants:

  1. Stocks are never purchases: zeroing other residential and non-residential property leaves SDLT, LBTT, LTT and their expected values unchanged.

  2. The main-residence flag alone never creates an additional-dwelling purchase. With that input unset, residential SDLT equals the main-rates tax on the main-residence price.

    The additional-dwelling input is a purchase already classified as a higher-rates transaction. A new main home bought while another dwelling is kept, with no replacement of a main residence, pays the higher rates (Sch 4ZA), so it belongs in that input, not the main-residence price. That classification limitation predates this PR.

  3. An additional-dwelling purchase adds exactly the higher-rates tax on its own price (nil below £40,000), and the higher rates are never below the main rates.

  4. SDLT never falls as the additional-dwelling price rises.

Mutation check. On main, invariants 1 and 2 fail. Invariant 2 leaves the input unset, so it exercises the old formula.

Targeted suites. 159 YAML tests under stamp duty, LTT, council tax and household pass.

National impact

Data: enhanced_frs_2024_25.h5@1.58.0, this branch against main at 6d89338 (this branch merged with it); the figures are unchanged from the run against e89b8ab, full microsimulation (£bn, branch minus main).

2025 2026 2027 2028 2029 2030
stamp_duty_land_tax −8.695 −9.115 −9.590 −10.068 −10.555 −11.095
land_and_buildings_transaction_tax −1.844 −1.925 −2.018 −2.112 −2.206 −2.309
expected_lbtt −0.083 −0.087 −0.091 −0.095 −0.099 −0.104
land_transaction_tax 0 0 0 0 0 0
additional-dwelling purchases −124.2 −128.3 −133.0 −137.6 −142.3 −147.5
non-residential purchases −19.0 −19.7 −20.4 −21.1 −21.8 −22.6
gov_tax / gov_balance −8.778 −9.201 −9.681 −10.163 −10.654 −11.199

What the figures cover:

  • 194k households gain, on average £45,214 in 2025 (190 sample records; four other records that drew a purchase hold under £40,000 of other property, below the higher-rates minimum, so gain nothing). No household loses.
  • Household SDLT goes from £16.32bn to £7.62bn in 2025: main-residence purchases plus £1.3m of SDLT on rents from four records.
  • LTT does not move: no Welsh sample household both drew a purchase and held other property.

Against HMRC. UK Stamp Tax Statistics 2024-25, England and Northern Ireland: Tables 6b and 8a.

On main, FY2024-25 residential SDLT was £14.75bn: £6.99bn on main residences and £7.76bn at the higher rates. The higher-rates part sat on 172k households with a £104bn base.

HMRC's 2024-25 figures:

  • 211,700 HRAD transactions worth £77.97bn, with £5.44bn of HRAD receipts, for all buyers including companies.
  • Residential receipts of £10.38bn, of which £9.15bn from individuals and £1.23bn from other buyers.

So the old base charged more higher-rates SDLT to households than HMRC collects from all HRAD buyers.

After this PR households pay no higher-rates SDLT unless a dataset supplies the purchases. Individuals paid at least £4.2bn of HRAD receipts in 2024-25 (£5.44bn less at most £1.23bn from non-individual buyers). That gap stays until the data imputes additional-dwelling purchases. The issue's other suggestion was imputing one purchase price, or a purchase probability on the stock, calibrated to HMRC's HRAD statistics. That imputation and calibration belongs in policyengine-uk-data, which owns the purchase draw: PolicyEngine/policyengine-uk-data#556.

For the replay, Autumn Budget 2024's 2pp HRAD rise becomes an inert leg on data without imputed purchases, rather than 2pp of the £94.7bn inferred base.

Fiscal-event replay: Autumn Budget 2024 HRAD +2pp

Real microsimulation runs of the replay construction on the proposed certified data (enhanced_frs_2024_25.h5@1.58.0), this branch against main 6d89338. The construction is copied from policyengine-scorecard pipeline/build_uk_event_registry.py on main, snapshotted 2026-10-11 (event_replay.py). Gain to the Exchequer, £bn.

Forward delta: higher-rates scale 5/7/10/15/17% from 31 October 2024. The processed tree samples 30 April, so FY2024 has no effect in either run.

stamp_duty_land_tax 2024 2025 2026 2027 2028 2029 2030
main 0 +2.191 +2.263 +2.345 +2.427 +2.510 +2.601
this PR 0 0 0 0 0 0 0

Input support: not met on 1.58.0, so the national exposure is inert. The dataset has no additional_residential_property_purchased, and this PR stops inferring it from the stock. The zero is that missing input, not an estimate of the measure. Read it as "not computable on this data", as the replay now does, never as an improvement on main's £2.2bn.

The input belongs in the uk-data d833 batch: PolicyEngine/policyengine-uk-data#556 proposes a seeded draw calibrated to HMRC's HRAD transactions. Its design (purchase rate, price distribution) is a methodology choice for Max, queued with the batch decision.

Not in this PR (found while fixing it)

Filed as their own issues:

axiom: n/a: input mapping only. No SDLT, LBTT or LTT rule changes; the Schedule 4ZA conditions and rates are untouched. rulespec-uk has no FA 2003 SDLT module (rulespec-uk#63).

🤖 Generated with Claude Code

Reproducer: policyengine_uk/tests/policy/baseline/gov/hmrc/stamp_duty/property_purchased_default.yaml::Issue 2238 - no purchase, no stamp duty, policyengine_uk/tests/policy/baseline/gov/hmrc/stamp_duty/property_purchased_default.yaml::Issue 2238 - buying the main residence does not charge the other-property stock, policyengine_uk/tests/policy/baseline/gov/hmrc/stamp_duty/property_purchased_default.yaml::Issue 2238 - an additional-dwelling purchase is charged on its own price, policyengine_uk/tests/test_property_transaction_tax_purchase_properties.py::test_property_stocks_are_never_purchases, policyengine_uk/tests/test_property_transaction_tax_purchase_properties.py::test_main_residence_flag_never_creates_an_additional_purchase, policyengine_uk/tests/test_property_transaction_tax_purchase_properties.py::test_additional_purchase_is_charged_on_its_own_price

…home

additional_residential_property_purchased was other_residential_property_value
times property_purchased, so any household that bought a home had its whole
stock of other residential property charged as one additional-dwelling
purchase at the higher rates; non_residential_property_purchased did the same
with the non-residential stock. property_purchased now sets only the
main-residence purchase. The additional-dwelling and non-residential purchase
prices are inputs, nil unless set directly or imputed by a dataset, and are
uprated like the stocks when a dataset supplies them.

Fixes #2238

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 10, 2026 07:57
Review round 1 on #2241: the main-residence flag alone never creates an
additional-dwelling purchase. A new main home bought while another dwelling
is kept pays the higher rates, so it belongs in the additional-dwelling
input, which holds purchases already classified as higher-rates transactions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

PE-UK main fix landed (UK hub, 2026-10-11 21:42Z): #2349 merged as ed7b42f9. It fixes test_dataframe_dataset.py::test_an_exported_frame_loads_back_and_exports_itself, which failed on every PR after #2251. Merge main into this branch and push once; don't re-run an old run, because re-runs use the original commit. If this PR's only Test failure was that test, the next run should be green.

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks right, fine to approve. The two should items are about landing it with the data, not the model code.

@MaxGhenis, the bug is real and the fix is the right shape. A purchase of a main residence is one land transaction (FA 2003 s. 55). It doesn't buy the household's stock of other property, and the old formula charged that whole stock at the higher rates. Turning the two purchase prices into plain inputs is the honest fix until the data imputes them. Nothing else in the model reads additional_residential_property_purchased or non_residential_property_purchased except the SDLT, LBTT and LTT transaction formulas.

  1. should: the uk-data SDLT calibration target will sit far above the household SDLT this PR leaves. policyengine-uk-data targets/sources/obr.py:264 maps the OBR "Stamp duty land tax" receipts line to stamp_duty_land_tax, and build_loss_matrix.py uses it as a plain GBP target in the national calibration.

    • Your own run has household SDLT falling from £16.32bn to £7.62bn in 2025.
    • PolicyEngine/policyengine-uk-data#564 already plans to take the corporate share out of that target. Even then, its 2026-27 household target is £12.17bn.
    • So the next data build on this model will try to close a gap of roughly £4bn in 2026-27 (about £8.2bn modelled against £12.17bn) by raising the weights of the 3.85% of households that drew a purchase. That would distort everything else those households carry, unless policyengine-uk-data#556 lands in the same batch.
    • Could the batch notes say that policyengine-uk-data#564's rescoped target goes in only together with policyengine-uk-data#556? Otherwise obr/sdlt should be dropped until the HRAD imputation is in.
  2. should: the 3.85% purchase draw now means a main-residence purchase, but its rate is all transactions. policyengine-uk-data datasets/frs.py:1853-1879 sets PROPERTY_PURCHASE_RATE = 0.0385 from 1.1m UK transactions a year. That includes the 211,700 HRAD transactions and the 124,500 residential purchases by non-individuals. This PR relabels property_purchased as "Main residence bought this year" (property_purchased.py:5). When policyengine-uk-data#556 adds a separate additional-dwelling draw, those HRAD transactions are counted twice unless the main-residence rate comes down. policyengine-uk-data#556 doesn't mention this yet. It is worth adding there rather than changing anything here.

  3. nit: a non-replacement main-residence purchase is still charged at main rates. Under FA 2003 Sch 4ZA, a buyer who keeps another dwelling and isn't replacing their main residence pays the higher rates on the new main home. You disclose this in the body as earlier than this PR. A one-line note in the main_residential_property_purchased docstring would keep it visible after merge.

Interaction with #2235. No file overlap, and the merge with main is clean. Together, the two PRs take 2025 SDLT from about £16.3bn of household SDLT plus a second copy of corporate SDLT down to about £7.6bn household plus £3.1bn corporate. #2322, stacked on #2235, would raise the corporate part to £4.8bn. Either way the model moves from above HMRC's total to below it until policyengine-uk-data#556 lands. That is the right direction for each fix on its own. It does mean the SDLT level will be low in any release cut between this merge and the data batch.

What I checked

  • I read the PR body, the full diff, the comments, and policyengine-uk-data#556 and policyengine-uk-data#564. There are no earlier reviews.
  • I read FA 2003 s. 55 and Sch 4ZA on legislation.gov.uk.
  • I hand-computed the YAML expectations. In 2025, £600,000 at main rates is 2,500 + 17,500 = £20,000. In 2022, £400,000 at the higher rates is 3,750 + 6,250 + 12,000 = £22,000 on top.
  • At head 5ac1df3f, in a fresh uv sync environment in a worktree:
    • YAML under gov/hmrc/stamp_duty, gov/wra and household: 155 passed;
    • test_property_transaction_tax_purchase_properties.py: 4 passed;
    • ruff check and ruff format --check on the 4 changed Python files are clean.
  • I reverted the two purchase variables to main and ran property_purchased_default.yaml again: the stock case fails, as it should.
  • I grepped the model for every reader of the two purchase variables. I grepped policyengine-uk-data main for anything that stores them (nothing does) and for SDLT calibration targets (finding 1).
  • Process: changelog.d/2238.fixed.md is the right type, and the Reproducer: and axiom: lines are present. Fix evidence CI passed.
  • I did not re-run the national tables or the replay. CI is green at this head.

🤖 Generated with Claude Code

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, per my review comment above.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub, 2026-10-15: adopted. The creating session has been idle since 10/10, and the Budget replay needs this before 28 October.

  • Main is merged (update-branch, head c7d2a6bf3). The old head 5ac1df3f4 predates the required Test / Result check, which is why the PR showed BLOCKED despite the approval.
  • Review. @vahid-ahmadi's approval covers 5ac1df3f4. An independent delta review of the merge with execution is running (Subfleet job 20261015-092320-uk2241-delta-review).
  • Landing waits on Max's d1354: whether the uk-data#556 additional-dwelling and non-residential purchase imputation lands in the same batch. Without it, household higher-rates SDLT is inert on the current data, which is Vahid's first "should".
  • Replay. The scorecard replay of 2.130.1 scores the AB2024 surcharge at +£11.69bn in PE against +£1.90bn from the OBR. This PR removes the property-stock charge behind that gap.

@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 69d9d18 Oct 15, 2026
14 checks passed
@MaxGhenis
MaxGhenis deleted the fix-additional-dwelling-purchase-2238 branch October 15, 2026 19:16
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Landed by the UK hub (merge queue, pinned to head c7d2a6bf3a20d50c213effd5273a642c58b1a07c).

  • Gates at this head: gh pr checks exits 0 (Test / Result and Lint included), mergeable, not a draft, no CHANGES_REQUESTED.
  • Reviews:
    • @vahid-ahmadi APPROVED 5ac1df3f4 (the PR's own diff).
    • The hub's independent GPT-6.1 Sol delta review with execution (Subfleet 20261015-092320-uk2241-delta-review) gave APPROVE WITH NITS at c7d2a6bf3. It found the model code right to land on its own merits, with no blocking defect in the PR or the merge delta. It ran 24 head tests and 60 independent MOCK scenarios, and the main controls fail as expected.
  • Impact, as the body states: household SDLT goes from £16.32bn to £7.62bn in 2025. Household higher-rates SDLT is inert on data without purchase inputs, read as "not computable on this data". The data side is Impute additional-dwelling and non-residential purchases, calibrated to HMRC's HRAD statistics policyengine-uk-data#556, whose batch landing is Max's d1354.
  • Follow-up (P3, documentation): the review's one nit is to describe additional_residential_property_purchased as the price of one transaction already classified as subject to the higher rates. A first buy-to-let can fail Schedule 4ZA's other-dwelling condition. It will be filed as a separate small PR.

MaxGhenis added a commit that referenced this pull request Oct 16, 2026
…into uc-hb-split-shared-cap-lha-rules

No conflicts. Main's two edits to files this PR also changes
(test_benefit_cap_specified_benefits_properties.py: REBALANCED_YEARS from
REBALANCING_YEARS; test_legacy_award_readers_properties.py: comment) merged
automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

additional_residential_property_purchased treats a household's whole other-property stock as an additional purchase

2 participants