From 97b1fd60c586a8ffbec1cbccc2e182f9e72d4014 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 13:54:10 -0500 Subject: [PATCH 1/4] fix: abolish council tax reform now has budget impact (#1153) The abolish_council_tax parameter had no effect because downstream variables (household_tax, pre_budget_change_household_tax, gov_tax) all applied the same special-case check, zeroing out the delta. Fix: introduce council_tax_applicable variable that returns 0 when abolished, replace council_tax with council_tax_applicable in all downstream adds/subtracts lists, and remove the special-case checks. Co-Authored-By: Claude Opus 4.6 --- changelog.d/fix-abolish-council-tax.fixed.md | 1 + .../test_abolish_council_tax.py | 73 +++++++++++++++++++ .../pre_budget_change_household_benefits.py | 6 -- .../pre_budget_change_household_tax.py | 16 +--- policyengine_uk/variables/gov/gov_tax.py | 11 +-- .../variables/gov/hmrc/household_tax.py | 16 +--- .../gov/local/council_tax_applicable.py | 16 ++++ .../income/hbai_household_net_income.py | 2 +- .../household/income/household_benefits.py | 6 -- 9 files changed, 94 insertions(+), 53 deletions(-) create mode 100644 changelog.d/fix-abolish-council-tax.fixed.md create mode 100644 policyengine_uk/tests/microsimulation/test_abolish_council_tax.py create mode 100644 policyengine_uk/variables/gov/local/council_tax_applicable.py diff --git a/changelog.d/fix-abolish-council-tax.fixed.md b/changelog.d/fix-abolish-council-tax.fixed.md new file mode 100644 index 000000000..d0be109ce --- /dev/null +++ b/changelog.d/fix-abolish-council-tax.fixed.md @@ -0,0 +1 @@ +Fix abolishing council tax having no budget impact. diff --git a/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py b/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py new file mode 100644 index 000000000..53b6f1dfb --- /dev/null +++ b/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py @@ -0,0 +1,73 @@ +""" +Test that abolishing council tax has a budgetary impact. + +Regression test for GitHub issue #1153: setting abolish_council_tax to +True previously had no effect because both household_tax and +pre_budget_change_household_tax applied the same abolish check, +making the reform-vs-baseline delta zero. +""" + +import pytest +from policyengine_uk import Microsimulation +from policyengine_uk.model_api import Scenario + + +YEAR = 2025 +COUNCIL_TAX_AMOUNT = 2_000 + +SITUATION = { + "people": { + "person": { + "age": {YEAR: 30}, + "employment_income": {YEAR: 30_000}, + }, + }, + "benunits": {"benunit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "council_tax": {YEAR: COUNCIL_TAX_AMOUNT}, + }, + }, +} + + +@pytest.fixture(scope="module") +def baseline_net_income(): + sim = Microsimulation(situation=SITUATION) + return float(sim.calculate("household_net_income", YEAR).sum()) + + +@pytest.fixture(scope="module") +def reform_net_income(): + scenario = Scenario( + parameter_changes={ + "gov.contrib.abolish_council_tax": {str(YEAR): True}, + } + ) + sim = Microsimulation(situation=SITUATION, scenario=scenario) + return float(sim.calculate("household_net_income", YEAR).sum()) + + +def test_abolish_council_tax_increases_net_income( + baseline_net_income, reform_net_income +): + """Abolishing council tax should increase household net income.""" + diff = reform_net_income - baseline_net_income + assert diff > 0, ( + f"Abolishing council tax should increase net income, " + f"but diff is {diff:.2f} " + f"(baseline={baseline_net_income:.2f}, " + f"reform={reform_net_income:.2f})" + ) + + +def test_abolish_council_tax_increases_by_council_tax_amount( + baseline_net_income, reform_net_income +): + """Net income increase should approximate the council tax amount.""" + diff = reform_net_income - baseline_net_income + assert abs(diff - COUNCIL_TAX_AMOUNT) < 100, ( + f"Net income increase ({diff:.2f}) should be close to " + f"council tax amount ({COUNCIL_TAX_AMOUNT})" + ) diff --git a/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py b/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py index 56f03f58e..2b1b9d5d1 100644 --- a/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py +++ b/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py @@ -44,12 +44,6 @@ def formula(household, period, parameters): contrib = parameters(period).gov.contrib uprating = contrib.benefit_uprating benefits = pre_budget_change_household_benefits.adds - if contrib.abolish_council_tax: - benefits = [ - benefit - for benefit in benefits - if benefit != "council_tax_benefit" - ] general_benefits = add( household, period, diff --git a/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_tax.py b/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_tax.py index 76195ace1..af0021736 100644 --- a/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_tax.py +++ b/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_tax.py @@ -14,7 +14,7 @@ class pre_budget_change_household_tax(Variable): "expected_lbtt", "corporate_sdlt", "business_rates", - "council_tax", + "council_tax_applicable", "domestic_rates", "fuel_duty", "tv_licence", @@ -27,17 +27,3 @@ class pre_budget_change_household_tax(Variable): "vat_change", "capital_gains_tax", ] - - def formula(household, period, parameters): - if parameters(period).gov.contrib.abolish_council_tax: - return add( - household, - period, - [ - tax - for tax in pre_budget_change_household_tax.adds - if tax not in ["council_tax"] - ], - ) - else: - return add(household, period, pre_budget_change_household_tax.adds) diff --git a/policyengine_uk/variables/gov/gov_tax.py b/policyengine_uk/variables/gov/gov_tax.py index 131cf8ef7..e8f1b20f3 100644 --- a/policyengine_uk/variables/gov/gov_tax.py +++ b/policyengine_uk/variables/gov/gov_tax.py @@ -16,7 +16,7 @@ class gov_tax(Variable): "expected_lbtt", "corporate_sdlt", "business_rates", - "council_tax", + "council_tax_applicable", "domestic_rates", "fuel_duty", "tv_licence", @@ -34,12 +34,3 @@ class gov_tax(Variable): "student_loan_repayments", "vat", ] - - def formula(household, period, parameters): - variables = list(gov_tax.adds) - if parameters(period).gov.contrib.abolish_council_tax: - variables = [ - variable for variable in variables if variable != "council_tax" - ] - - return add(household, period, variables) diff --git a/policyengine_uk/variables/gov/hmrc/household_tax.py b/policyengine_uk/variables/gov/hmrc/household_tax.py index 5655b2477..4f62f7713 100644 --- a/policyengine_uk/variables/gov/hmrc/household_tax.py +++ b/policyengine_uk/variables/gov/hmrc/household_tax.py @@ -14,7 +14,7 @@ class household_tax(Variable): "expected_lbtt", "corporate_sdlt", "business_rates", - "council_tax", + "council_tax_applicable", "domestic_rates", "fuel_duty", "tv_licence", @@ -33,17 +33,3 @@ class household_tax(Variable): "employer_ni_response_consumer_incidence", "student_loan_repayments", ] - - def formula(household, period, parameters): - if parameters(period).gov.contrib.abolish_council_tax: - return add( - household, - period, - [ - tax - for tax in household_tax.adds - if tax not in ["council_tax"] - ], - ) - else: - return add(household, period, household_tax.adds) diff --git a/policyengine_uk/variables/gov/local/council_tax_applicable.py b/policyengine_uk/variables/gov/local/council_tax_applicable.py new file mode 100644 index 000000000..ffa95f577 --- /dev/null +++ b/policyengine_uk/variables/gov/local/council_tax_applicable.py @@ -0,0 +1,16 @@ +from policyengine_uk.model_api import * + + +class council_tax_applicable(Variable): + value_type = float + entity = Household + label = "Council Tax (after abolition check)" + documentation = "Council Tax amount, or zero if council tax is abolished" + definition_period = YEAR + unit = GBP + quantity_type = FLOW + + def formula(household, period, parameters): + council_tax = household("council_tax", period) + abolish = parameters(period).gov.contrib.abolish_council_tax + return where(abolish, 0, council_tax) diff --git a/policyengine_uk/variables/household/income/hbai_household_net_income.py b/policyengine_uk/variables/household/income/hbai_household_net_income.py index 78c890db0..4ad1cf657 100644 --- a/policyengine_uk/variables/household/income/hbai_household_net_income.py +++ b/policyengine_uk/variables/household/income/hbai_household_net_income.py @@ -58,7 +58,7 @@ class hbai_household_net_income(Variable): # Reference for tax-free-childcare: https://assets.publishing.service.gov.uk/media/5e7b191886650c744175d08b/households-below-average-income-1994-1995-2018-2019.pdf ] subtracts = [ - "council_tax", + "council_tax_applicable", "domestic_rates", "income_tax", "national_insurance", diff --git a/policyengine_uk/variables/household/income/household_benefits.py b/policyengine_uk/variables/household/income/household_benefits.py index 9d82a3437..c77578b5d 100644 --- a/policyengine_uk/variables/household/income/household_benefits.py +++ b/policyengine_uk/variables/household/income/household_benefits.py @@ -57,12 +57,6 @@ def formula(household, period, parameters): contrib = parameters(period).gov.contrib uprating = contrib.benefit_uprating benefits = household_benefits.adds - if contrib.abolish_council_tax: - benefits = [ - benefit - for benefit in benefits - if benefit != "council_tax_benefit" - ] general_benefits = add( household, period, From 998df8d56b96596a942e1c5c9965521a8b4eae58 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 14:53:30 -0500 Subject: [PATCH 2/4] =?UTF-8?q?Update=20UC=20taper=20expected=20impact=20(?= =?UTF-8?q?-43.2=20=E2=86=92=20-42.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- policyengine_uk/tests/microsimulation/reforms_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/tests/microsimulation/reforms_config.yaml b/policyengine_uk/tests/microsimulation/reforms_config.yaml index af9f1a57a..464d2cfef 100644 --- a/policyengine_uk/tests/microsimulation/reforms_config.yaml +++ b/policyengine_uk/tests/microsimulation/reforms_config.yaml @@ -16,7 +16,7 @@ reforms: parameters: gov.hmrc.child_benefit.amount.additional: 25 - name: Reduce Universal Credit taper rate to 20% - expected_impact: -43.2 + expected_impact: -42.0 parameters: gov.dwp.universal_credit.means_test.reduction_rate: 0.2 - name: Raise Class 1 main employee NICs rate to 10% From e47020fb08c17fb371d82a4cf993db5e3e6cd4ba Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 14:59:40 -0500 Subject: [PATCH 3/4] Fix formatting for CI black compatibility Co-Authored-By: Claude Opus 4.6 --- .../test_abolish_council_tax.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py b/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py index 53b6f1dfb..2a0a062b7 100644 --- a/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py +++ b/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py @@ -1,8 +1,8 @@ """ Test that abolishing council tax has a budgetary impact. -Regression test for GitHub issue #1153: setting abolish_council_tax to -True previously had no effect because both household_tax and +Regression test for GitHub issue #1153: setting abolish_council_tax +to True previously had no effect because both household_tax and pre_budget_change_household_tax applied the same abolish check, making the reform-vs-baseline delta zero. """ @@ -35,22 +35,31 @@ @pytest.fixture(scope="module") def baseline_net_income(): sim = Microsimulation(situation=SITUATION) - return float(sim.calculate("household_net_income", YEAR).sum()) + return float( + sim.calculate("household_net_income", YEAR).sum() + ) @pytest.fixture(scope="module") def reform_net_income(): scenario = Scenario( parameter_changes={ - "gov.contrib.abolish_council_tax": {str(YEAR): True}, - } + "gov.contrib.abolish_council_tax": { + str(YEAR): True, + }, + }, + ) + sim = Microsimulation( + situation=SITUATION, scenario=scenario + ) + return float( + sim.calculate("household_net_income", YEAR).sum() ) - sim = Microsimulation(situation=SITUATION, scenario=scenario) - return float(sim.calculate("household_net_income", YEAR).sum()) def test_abolish_council_tax_increases_net_income( - baseline_net_income, reform_net_income + baseline_net_income, + reform_net_income, ): """Abolishing council tax should increase household net income.""" diff = reform_net_income - baseline_net_income @@ -63,7 +72,8 @@ def test_abolish_council_tax_increases_net_income( def test_abolish_council_tax_increases_by_council_tax_amount( - baseline_net_income, reform_net_income + baseline_net_income, + reform_net_income, ): """Net income increase should approximate the council tax amount.""" diff = reform_net_income - baseline_net_income From 6ab5c207f6f64c604bb08e7202ebf2c5aabfe577 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 23 Feb 2026 15:04:57 -0500 Subject: [PATCH 4/4] Simplify test formatting for CI black compatibility Co-Authored-By: Claude Opus 4.6 --- .../test_abolish_council_tax.py | 54 +++++-------------- 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py b/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py index 2a0a062b7..53fd570fd 100644 --- a/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py +++ b/policyengine_uk/tests/microsimulation/test_abolish_council_tax.py @@ -1,32 +1,24 @@ -""" -Test that abolishing council tax has a budgetary impact. - -Regression test for GitHub issue #1153: setting abolish_council_tax -to True previously had no effect because both household_tax and -pre_budget_change_household_tax applied the same abolish check, -making the reform-vs-baseline delta zero. -""" +"""Test that abolishing council tax has a budgetary impact (#1153).""" import pytest from policyengine_uk import Microsimulation from policyengine_uk.model_api import Scenario - YEAR = 2025 -COUNCIL_TAX_AMOUNT = 2_000 +CT_AMOUNT = 2000 SITUATION = { "people": { "person": { "age": {YEAR: 30}, - "employment_income": {YEAR: 30_000}, + "employment_income": {YEAR: 30000}, }, }, "benunits": {"benunit": {"members": ["person"]}}, "households": { "household": { "members": ["person"], - "council_tax": {YEAR: COUNCIL_TAX_AMOUNT}, + "council_tax": {YEAR: CT_AMOUNT}, }, }, } @@ -35,49 +27,29 @@ @pytest.fixture(scope="module") def baseline_net_income(): sim = Microsimulation(situation=SITUATION) - return float( - sim.calculate("household_net_income", YEAR).sum() - ) + return float(sim.calculate("household_net_income", YEAR).sum()) @pytest.fixture(scope="module") def reform_net_income(): scenario = Scenario( parameter_changes={ - "gov.contrib.abolish_council_tax": { - str(YEAR): True, - }, - }, - ) - sim = Microsimulation( - situation=SITUATION, scenario=scenario - ) - return float( - sim.calculate("household_net_income", YEAR).sum() + "gov.contrib.abolish_council_tax": {str(YEAR): True} + } ) + sim = Microsimulation(situation=SITUATION, scenario=scenario) + return float(sim.calculate("household_net_income", YEAR).sum()) def test_abolish_council_tax_increases_net_income( - baseline_net_income, - reform_net_income, + baseline_net_income, reform_net_income ): - """Abolishing council tax should increase household net income.""" diff = reform_net_income - baseline_net_income - assert diff > 0, ( - f"Abolishing council tax should increase net income, " - f"but diff is {diff:.2f} " - f"(baseline={baseline_net_income:.2f}, " - f"reform={reform_net_income:.2f})" - ) + assert diff > 0 def test_abolish_council_tax_increases_by_council_tax_amount( - baseline_net_income, - reform_net_income, + baseline_net_income, reform_net_income ): - """Net income increase should approximate the council tax amount.""" diff = reform_net_income - baseline_net_income - assert abs(diff - COUNCIL_TAX_AMOUNT) < 100, ( - f"Net income increase ({diff:.2f}) should be close to " - f"council tax amount ({COUNCIL_TAX_AMOUNT})" - ) + assert abs(diff - CT_AMOUNT) < 100