Skip to content

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

Open
MaxGhenis wants to merge 5 commits into
mainfrom
fix-additional-dwelling-purchase-2238
Open

MaxGhenis wants to merge 5 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.

This branch has not been deployed

No deployments
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

1 participant