From 9acee97d3d4656c4d6f1536d1d35bb506876f763 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 24 Sep 2026 20:04:29 -0400 Subject: [PATCH 1/4] Remove the deprecated structured mortgage-interest inputs Stage 3 of the atomic-inputs migration (#9275; stages 1-2 were #9276 and the deprecation window). first_home_mortgage_interest and second_home_mortgage_interest are deleted; the person-level home_mortgage_interest input is the single mortgage-interest input and home_mortgage_interest_tax_unit becomes a plain adds over it. The per-mortgage balance and origination-year inputs are unchanged - the section 163(h)(3)(F) caps need them per loan, and loans are jointly held, so tax_unit remains their natural entity. Safety basis, replacing the original wait-for-the-data-swap plan (microcosm#673 re-scoped 2026-09-24): the certified engine ABI pins policyengine-us 2.2.1, which already prefers the person-level column whenever both are present, so datasets carrying the removed columns compute identically; and policyengine-core >= 3.31.0 (now the floor) warns instead of silently skipping unknown dataset columns, so stale h5s announce the removal rather than losing data quietly. API households that explicitly supplied the removed inputs now get an unknown-variable error - the intended end state: one fact, one input. Cap tests converted to the person-level input (identical expected values; the formula only ever used the total); the structural-only and double-supply cases are inexpressible by design and removed. axiom: n/a (input-surface/infra change; no policy rule altered) Co-Authored-By: Claude Fable 5 --- changelog.d/mortgage-atomic-stage3.removed.md | 1 + .../deductible_mortgage_interest.yaml | 55 ++----------- .../tax_unit/mortgage_interest_structure.py | 50 ++---------- pyproject.toml | 2 +- uv.lock | 78 +++++++++---------- 5 files changed, 52 insertions(+), 134 deletions(-) create mode 100644 changelog.d/mortgage-atomic-stage3.removed.md diff --git a/changelog.d/mortgage-atomic-stage3.removed.md b/changelog.d/mortgage-atomic-stage3.removed.md new file mode 100644 index 00000000000..a54ee47990d --- /dev/null +++ b/changelog.d/mortgage-atomic-stage3.removed.md @@ -0,0 +1 @@ +Removed the deprecated first_home_mortgage_interest and second_home_mortgage_interest inputs; the person-level home_mortgage_interest input is the single mortgage-interest input, while the per-mortgage balance and origination-year inputs are unchanged. Datasets still carrying the removed columns load with a warning on policyengine-core 3.31+, which is now the minimum. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml index c437e3030f3..2b91aad3780 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml @@ -5,7 +5,7 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 900_000 - first_home_mortgage_interest: 45_000 + home_mortgage_interest: 45_000 first_home_mortgage_origination_year: 2017 output: deductible_mortgage_interest_tax_unit: 45_000 @@ -19,7 +19,7 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 900_000 - first_home_mortgage_interest: 45_000 + home_mortgage_interest: 45_000 first_home_mortgage_origination_year: 2018 output: deductible_mortgage_interest_tax_unit: 37_500 @@ -34,10 +34,9 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 800_000 - first_home_mortgage_interest: 40_000 + home_mortgage_interest: 55_000 first_home_mortgage_origination_year: 2017 second_home_mortgage_balance: 300_000 - second_home_mortgage_interest: 15_000 second_home_mortgage_origination_year: 2018 output: deductible_mortgage_interest_tax_unit: 40_000 @@ -51,14 +50,14 @@ tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 500_000 - first_home_mortgage_interest: 25_000 + home_mortgage_interest: 25_000 first_home_mortgage_origination_year: 2018 output: deductible_mortgage_interest_tax_unit: 18_750 interest_deduction: 18_750 mortgage_interest: 25_000 -- name: No structural mortgage inputs yields zero deductible interest +- name: No mortgage inputs yields zero deductible interest period: 2024 input: filing_status: SINGLE @@ -68,7 +67,7 @@ deductible_mortgage_interest_tax_unit: 0 non_deductible_mortgage_interest_tax_unit: 0 -- name: Person-level home mortgage interest is deductible when no structural inputs are provided +- name: Person-level home mortgage interest is the single interest input period: 2024 input: people: @@ -85,48 +84,6 @@ interest_deduction: 10_000 mortgage_interest: 10_000 -- name: Structural interest with no reported balance is fully deductible - period: 2024 - input: - filing_status: SINGLE - tax_unit_itemizes: true - standard_deduction: 0 - first_home_mortgage_interest: 10_000 - output: - deductible_mortgage_interest_tax_unit: 10_000 - -- name: Regression guard - post-TCJA balance above the cap is still limited - period: 2024 - input: - filing_status: SINGLE - tax_unit_itemizes: true - standard_deduction: 0 - first_home_mortgage_balance: 900_000 - first_home_mortgage_interest: 45_000 - first_home_mortgage_origination_year: 2018 - output: - deductible_mortgage_interest_tax_unit: 37_500 - -- name: Person-level input is canonical when both input styles are supplied (issue #9275) - period: 2024 - input: - people: - head: - home_mortgage_interest: 12_000 - tax_units: - tax_unit: - members: [head] - filing_status: SINGLE - tax_unit_itemizes: true - standard_deduction: 0 - first_home_mortgage_interest: 10_000 - output: - # Person-level wins; the deprecated structured input is ignored when - # person-level interest is reported. - home_mortgage_interest_tax_unit: 12_000 - deductible_mortgage_interest_tax_unit: 12_000 - interest_deduction: 12_000 - - name: Person-level interest still passes through the acquisition-debt caps period: 2024 input: diff --git a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py index fed7ca75751..b941c60ff5f 100644 --- a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py +++ b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py @@ -52,38 +52,6 @@ class second_home_mortgage_balance(Variable): ) -class first_home_mortgage_interest(Variable): - value_type = float - entity = TaxUnit - label = "First home mortgage interest" - unit = USD - definition_period = YEAR - default_value = 0 - documentation = ( - "DEPRECATED (issue #9275): use the person-level home_mortgage_interest " - "input instead; the deduction only ever uses the first+second sum, and " - "this input is read only when no person-level interest is reported. " - "Kept temporarily so existing datasets that supply it keep working; " - "removal is scheduled once certified microdata stops exporting it." - ) - - -class second_home_mortgage_interest(Variable): - value_type = float - entity = TaxUnit - label = "Second home mortgage interest" - unit = USD - definition_period = YEAR - default_value = 0 - documentation = ( - "DEPRECATED (issue #9275): use the person-level home_mortgage_interest " - "input instead; the deduction only ever uses the first+second sum, and " - "this input is read only when no person-level interest is reported. " - "Kept temporarily so existing datasets that supply it keep working; " - "removal is scheduled once certified microdata stops exporting it." - ) - - class first_home_mortgage_origination_year(Variable): value_type = int entity = TaxUnit @@ -111,20 +79,12 @@ class home_mortgage_interest_tax_unit(Variable): unit = USD definition_period = YEAR documentation = ( - "Total home mortgage interest. The person-level home_mortgage_interest " - "input is canonical; the deprecated structured first/second interest " - "inputs are used only when no person-level interest is reported " - "(existing datasets still supply them — see issue #9275)." + "Total home mortgage interest, summed from the person-level " + "home_mortgage_interest input — the single mortgage-interest input " + "(issue #9275). The per-mortgage balances and origination years remain " + "tax-unit inputs because the acquisition-debt caps need them per loan." ) - - def formula(tax_unit, period, parameters): - reported_interest = add(tax_unit, period, ["home_mortgage_interest"]) - structured_interest = add( - tax_unit, - period, - ["first_home_mortgage_interest", "second_home_mortgage_interest"], - ) - return where(reported_interest > 0, reported_interest, structured_interest) + adds = ["home_mortgage_interest"] class deductible_mortgage_interest_tax_unit(Variable): diff --git a/pyproject.toml b/pyproject.toml index 80914f6de6f..8e552054050 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ # pandas 3 copy-on-write enables the shallow-copy fast path in dataset # extension and is available on every supported Python version. "pandas>=3.0", - "policyengine-core>=3.30.1", + "policyengine-core>=3.31.0", "spm-calculator>=1.0.0,<=1.0.0.post1", "tables>=3.9", "tqdm>=4.67.1", diff --git a/uv.lock b/uv.lock index 5c67d438bbb..e40f51bc4e4 100644 --- a/uv.lock +++ b/uv.lock @@ -1663,7 +1663,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1703,7 +1703,7 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.30.2" +version = "3.32.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, @@ -1723,9 +1723,9 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/6b/f3e845e64401f329d2dd43ed4ea4f6663810b0e22879e13e3cec997caeab/policyengine_core-3.30.2.tar.gz", hash = "sha256:1272a08c432e3c26eb0cc6eabdabed1cf1bf16a837c006d006816ad8e665a33d", size = 501156, upload-time = "2026-07-20T12:20:58.184Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/86/52f789bcf42c1593988e36b3499c49e38f1fc77f4efdeb59a11fd5733bf9/policyengine_core-3.32.6.tar.gz", hash = "sha256:5a19a6940d5318648063bec5daddaf68d732e964e25c14cc2d9c8452aa831461", size = 421867, upload-time = "2026-09-16T18:11:17.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/6e/f8b6af58c701a05d4afd2956747364618bc4ffdf40f37b28d450b2223a27/policyengine_core-3.30.2-py3-none-any.whl", hash = "sha256:ba187b4cf459c337c4bfe02b1f6b2491d45240ea1bd41a1148a1ec9ca8c51fe9", size = 245046, upload-time = "2026-07-20T12:20:56.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/39/b52ce31a2ae2a70d9cc5035eaeb75506cc096c43599177dd9dc7bf88db49/policyengine_core-3.32.6-py3-none-any.whl", hash = "sha256:a29acfc7e3feb4bc311a03440c8576df7dd5b9793e81a5dcf95169b473dd6b90", size = 246251, upload-time = "2026-09-16T18:11:16.01Z" }, ] [[package]] @@ -1760,7 +1760,7 @@ requires-dist = [ { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=1.0.4.post1" }, { name = "microdf-python", specifier = ">=1.0.0" }, { name = "pandas", specifier = ">=3.0" }, - { name = "policyengine-core", specifier = ">=3.30.1" }, + { name = "policyengine-core", specifier = ">=3.31.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9.0" }, { name = "setuptools", marker = "extra == 'dev'", specifier = ">=80.9.0" }, { name = "spm-calculator", specifier = ">=1.0.0,<=1.0.0.post1" }, @@ -2315,23 +2315,23 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -2351,23 +2351,23 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ From a412637dbb21a372c0c168059ed2f4cab640c9df Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 24 Sep 2026 20:10:24 -0400 Subject: [PATCH 2/4] Renormalize uv.lock with the pinned uv 0.12.13 The previous commit's lock was rewritten by an unpinned local uv (0.11.7) during a `uv run` auto-sync, which the release rehearsal rejects as a reviewed-graph change (.github/release-lock.md). Relock with the workflow-pinned 0.12.13 via the documented version-bump/ restore re-resolution; `release_lock.py --rehearse` passes under 0.12.13 with this lock. Co-Authored-By: Claude Fable 5 --- uv.lock | 70 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/uv.lock b/uv.lock index e40f51bc4e4..dd60f548b38 100644 --- a/uv.lock +++ b/uv.lock @@ -1663,7 +1663,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2315,23 +2315,23 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.12'" }, - { name = "babel", marker = "python_full_version < '3.12'" }, - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.12'" }, - { name = "imagesize", marker = "python_full_version < '3.12'" }, - { name = "jinja2", marker = "python_full_version < '3.12'" }, - { name = "packaging", marker = "python_full_version < '3.12'" }, - { name = "pygments", marker = "python_full_version < '3.12'" }, - { name = "requests", marker = "python_full_version < '3.12'" }, - { name = "roman-numerals", marker = "python_full_version < '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -2351,23 +2351,23 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ From df83a5f06da1d7d9047884b4b017616311ecd8bf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 25 Sep 2026 17:04:45 -0400 Subject: [PATCH 3/4] Address stage-3 review: stale comments, tests, loader-warning test, changelog - Remove the stale fallback comment in the deductible formula and the "deprecated" wording in the uprating-series test. - Document the filer-allocation convention in home_mortgage_interest_share: shares follow the head's and spouse's own reported interest, or split evenly when only other members report it; shares sum to one, so person amounts always add back to the tax-unit totals. - Tests: joint spouse split (sum and allocation), dependent-only reporter, smaller grandfathered loan with a larger new loan, MFS $500k grandfathered cap, combined grandfathered balances above $1M, and non-deductible interest pinned on capped cases; the duplicated post-TCJA case is replaced. - Add a dataset test proving the removed columns are named by the loader warning and ignored in favor of the person-level input. - Changelog: state the API break and the zero-interest edge case. Co-Authored-By: Claude Opus 5.5 --- changelog.d/mortgage-atomic-stage3.removed.md | 2 +- .../tests/core/test_uprating_series.py | 5 +- .../test_removed_mortgage_interest_inputs.py | 64 ++++++++++++++ .../deductible_mortgage_interest.yaml | 88 +++++++++++++++++-- .../person/home_mortgage_interest_share.py | 9 +- .../tax_unit/mortgage_interest_structure.py | 2 - 6 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 policyengine_us/tests/microsimulation/data/test_removed_mortgage_interest_inputs.py diff --git a/changelog.d/mortgage-atomic-stage3.removed.md b/changelog.d/mortgage-atomic-stage3.removed.md index a54ee47990d..ce43e4949f8 100644 --- a/changelog.d/mortgage-atomic-stage3.removed.md +++ b/changelog.d/mortgage-atomic-stage3.removed.md @@ -1 +1 @@ -Removed the deprecated first_home_mortgage_interest and second_home_mortgage_interest inputs; the person-level home_mortgage_interest input is the single mortgage-interest input, while the per-mortgage balance and origination-year inputs are unchanged. Datasets still carrying the removed columns load with a warning on policyengine-core 3.31+, which is now the minimum. +Removed the deprecated first_home_mortgage_interest and second_home_mortgage_interest inputs; the person-level home_mortgage_interest input is now the only mortgage-interest input, while the per-mortgage balance and origination-year inputs are unchanged. API callers that pass either removed input now receive an unknown-variable error and should supply home_mortgage_interest on the filer(s) instead. Datasets that still carry the removed columns load with policyengine-core's unknown-column warning (3.31.0 is now the minimum), and any tax unit whose interest appeared only in those columns now computes zero mortgage interest; the certified default dataset has no such tax units. diff --git a/policyengine_us/tests/core/test_uprating_series.py b/policyengine_us/tests/core/test_uprating_series.py index 06595785de9..d4f408a4071 100644 --- a/policyengine_us/tests/core/test_uprating_series.py +++ b/policyengine_us/tests/core/test_uprating_series.py @@ -57,9 +57,8 @@ # Benefits adjusted by the Social Security cost-of-living index. "veterans_benefits": COLA, # Consumption expenses, and the interest on debts, follow prices. The - # structured first and second mortgage inputs stay on the default: two are - # deprecated (issue #9275) and the file's formulas are outside the - # selective coverage job. + # per-mortgage balance and origination-year inputs stay on the default: + # the file's formulas are outside the selective coverage job. "pre_subsidy_rent": CPI_U, "pre_subsidy_care_expenses": CPI_U, "spm_unit_pre_subsidy_childcare_expenses": CPI_U, diff --git a/policyengine_us/tests/microsimulation/data/test_removed_mortgage_interest_inputs.py b/policyengine_us/tests/microsimulation/data/test_removed_mortgage_interest_inputs.py new file mode 100644 index 00000000000..209cad5d210 --- /dev/null +++ b/policyengine_us/tests/microsimulation/data/test_removed_mortgage_interest_inputs.py @@ -0,0 +1,64 @@ +""" +Datasets built before issue #9275's stage 3 can still carry the removed +tax-unit ``first_home_mortgage_interest`` / ``second_home_mortgage_interest`` +columns. Loading one must name the ignored columns (policyengine-core's +unknown-column warning) and compute mortgage interest from the person-level +``home_mortgage_interest`` input alone. +""" + +import logging + +import pandas as pd + +from policyengine_us import Microsimulation +from policyengine_us.data.dataset_schema import USSingleYearDataset + + +def _dataset_with_removed_columns() -> USSingleYearDataset: + return USSingleYearDataset( + person=pd.DataFrame( + { + "person_id": [1, 2], + "age": [40.0, 40.0], + "employment_income": [50_000.0, 0.0], + "person_household_id": [1, 1], + "person_tax_unit_id": [1, 1], + "person_spm_unit_id": [1, 1], + "person_family_id": [1, 1], + "person_marital_unit_id": [1, 1], + "home_mortgage_interest": [900.0, 100.0], + } + ), + household=pd.DataFrame( + {"household_id": [1], "household_weight": [1.0], "state_fips": [6]} + ), + tax_unit=pd.DataFrame( + { + "tax_unit_id": [1], + # Deliberately different from the person-level total so the + # test proves the removed columns are ignored. + "first_home_mortgage_interest": [5_000.0], + "second_home_mortgage_interest": [2_000.0], + } + ), + spm_unit=pd.DataFrame({"spm_unit_id": [1]}), + family=pd.DataFrame({"family_id": [1]}), + marital_unit=pd.DataFrame({"marital_unit_id": [1]}), + time_period=2024, + ) + + +def test_removed_mortgage_interest_columns_warn_and_are_ignored(caplog): + with caplog.at_level(logging.WARNING): + sim = Microsimulation(dataset=_dataset_with_removed_columns()) + interest = sim.calculate("home_mortgage_interest_tax_unit", 2024) + + warnings = [ + record.getMessage() + for record in caplog.records + if "do not match any variable" in record.getMessage() + ] + assert len(warnings) == 1 + assert "first_home_mortgage_interest" in warnings[0] + assert "second_home_mortgage_interest" in warnings[0] + assert interest.values.tolist() == [1_000.0] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml index 2b91aad3780..4e844efa46b 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/deductible_mortgage_interest.yaml @@ -25,6 +25,8 @@ deductible_mortgage_interest_tax_unit: 37_500 interest_deduction: 37_500 mortgage_interest: 45_000 + # 45,000 x (1 - 750,000 / 900,000) + non_deductible_mortgage_interest_tax_unit: 7_500 - name: Mixed-vintage mortgages use the combined statutory balance limit period: 2024 @@ -84,20 +86,96 @@ interest_deduction: 10_000 mortgage_interest: 10_000 -- name: Person-level interest still passes through the acquisition-debt caps +- name: Joint filers' interest sums across spouses and is allocated back by reported shares period: 2024 input: people: head: - home_mortgage_interest: 45_000 + age: 45 + home_mortgage_interest: 30_000 + spouse: + age: 45 + home_mortgage_interest: 15_000 tax_units: tax_unit: - members: [head] - filing_status: SINGLE + members: [head, spouse] + filing_status: JOINT tax_unit_itemizes: true standard_deduction: 0 first_home_mortgage_balance: 900_000 first_home_mortgage_origination_year: 2018 output: - # Post-TCJA cap: 45,000 x 750,000 / 900,000 = 37,500 + home_mortgage_interest_tax_unit: 45_000 + # 45,000 x 750,000 / 900,000 = 37,500, split 2:1 by reported interest + deductible_mortgage_interest_tax_unit: 37_500 + non_deductible_mortgage_interest_tax_unit: 7_500 + deductible_mortgage_interest: [25_000, 12_500] + non_deductible_mortgage_interest: [5_000, 2_500] + +- name: Interest reported only by a dependent is claimed by the filers in equal shares + period: 2024 + input: + people: + head: + age: 45 + spouse: + age: 45 + child: + age: 10 + home_mortgage_interest: 6_000 + tax_units: + tax_unit: + members: [head, spouse, child] + filing_status: JOINT + tax_unit_itemizes: true + standard_deduction: 0 + output: + home_mortgage_interest_tax_unit: 6_000 + deductible_mortgage_interest_tax_unit: 6_000 + deductible_mortgage_interest: [3_000, 3_000, 0] + +- name: Smaller grandfathered loan reduces the post-TCJA limit on a larger new loan + period: 2024 + input: + filing_status: JOINT + tax_unit_itemizes: true + standard_deduction: 0 + home_mortgage_interest: 45_000 + first_home_mortgage_balance: 200_000 + first_home_mortgage_origination_year: 2017 + second_home_mortgage_balance: 700_000 + second_home_mortgage_origination_year: 2018 + output: + # 26 USC 163(h)(3)(F)(iii): 200,000 grandfathered + min(700,000, 750,000 - 200,000) + # = 750,000 qualified of 900,000; 45,000 x 750 / 900 = 37,500 deductible_mortgage_interest_tax_unit: 37_500 + +- name: Married filing separately keeps the 500,000 grandfathered cap + period: 2024 + input: + filing_status: SEPARATE + tax_unit_itemizes: true + standard_deduction: 0 + home_mortgage_interest: 30_000 + first_home_mortgage_balance: 600_000 + first_home_mortgage_origination_year: 2016 + output: + # 30,000 x 500,000 / 600,000 + deductible_mortgage_interest_tax_unit: 25_000 + non_deductible_mortgage_interest_tax_unit: 5_000 + +- name: Combined grandfathered balances above 1,000,000 are limited in total + period: 2024 + input: + filing_status: JOINT + tax_unit_itemizes: true + standard_deduction: 0 + home_mortgage_interest: 60_000 + first_home_mortgage_balance: 800_000 + first_home_mortgage_origination_year: 2015 + second_home_mortgage_balance: 400_000 + second_home_mortgage_origination_year: 2016 + output: + # 60,000 x 1,000,000 / 1,200,000 + deductible_mortgage_interest_tax_unit: 50_000 + non_deductible_mortgage_interest_tax_unit: 10_000 diff --git a/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py b/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py index 7cb833e0b3f..3f567c3e097 100644 --- a/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py +++ b/policyengine_us/variables/household/expense/person/home_mortgage_interest_share.py @@ -7,9 +7,12 @@ class home_mortgage_interest_share(Variable): label = "Share of tax-unit home mortgage interest" definition_period = YEAR documentation = ( - "Allocates tax-unit mortgage interest across filers using reported " - "person-level home mortgage interest when available, otherwise evenly " - "across head and spouse." + "Allocates the tax unit's mortgage interest (and its deductible and " + "non-deductible parts) to the filers, who claim the deduction: in " + "proportion to the head's and spouse's own reported interest, or " + "evenly between them when only other members (such as dependents) " + "report interest. Shares sum to one across head and spouse, so the " + "person amounts always add back to the tax-unit totals." ) def formula(person, period, parameters): diff --git a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py index b941c60ff5f..f161583577c 100644 --- a/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py +++ b/policyengine_us/variables/household/expense/tax_unit/mortgage_interest_structure.py @@ -105,8 +105,6 @@ def formula(tax_unit, period, parameters): first_year = tax_unit("first_home_mortgage_origination_year", period) second_year = tax_unit("second_home_mortgage_origination_year", period) total_balance = first_balance + second_balance - # Falls back to reported person-level interest when the structured - # first/second inputs are absent. total_interest = tax_unit("home_mortgage_interest_tax_unit", period) filing_status = tax_unit("filing_status", period) From 3aa0a33f56708e0c8528e588067361e3aeead7cf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 20:20:13 -0400 Subject: [PATCH 4/4] Drop the removed mortgage-interest inputs from the PolicyBench IRA cases Main gained policybench_ira.yaml (#10032) after this PR's last CI run, and three of its cases still pass first_home_mortgage_interest / second_home_mortgage_interest. With the inputs deleted those cases would fail to parse, so merging on the stale green run would have broken main. Each case also carries person-level home_mortgage_interest equal to the structured total (2,743; 7,566; 2,975.72 + 768.28 = 3,744), and main's formula already prefers the person-level sum whenever it is positive, so the four structured lines were inert there. Removing them leaves every expected value unchanged. Also drop "(3.31.0 is now the minimum)" from the changelog: this PR no longer changes the policyengine-core floor, which main sets at 3.32.8. Co-Authored-By: Claude Opus 5.5 --- changelog.d/mortgage-atomic-stage3.removed.md | 2 +- .../tests/policy/baseline/gov/irs/ira/policybench_ira.yaml | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/changelog.d/mortgage-atomic-stage3.removed.md b/changelog.d/mortgage-atomic-stage3.removed.md index ce43e4949f8..f67c2d84d32 100644 --- a/changelog.d/mortgage-atomic-stage3.removed.md +++ b/changelog.d/mortgage-atomic-stage3.removed.md @@ -1 +1 @@ -Removed the deprecated first_home_mortgage_interest and second_home_mortgage_interest inputs; the person-level home_mortgage_interest input is now the only mortgage-interest input, while the per-mortgage balance and origination-year inputs are unchanged. API callers that pass either removed input now receive an unknown-variable error and should supply home_mortgage_interest on the filer(s) instead. Datasets that still carry the removed columns load with policyengine-core's unknown-column warning (3.31.0 is now the minimum), and any tax unit whose interest appeared only in those columns now computes zero mortgage interest; the certified default dataset has no such tax units. +Removed the deprecated first_home_mortgage_interest and second_home_mortgage_interest inputs; the person-level home_mortgage_interest input is now the only mortgage-interest input, while the per-mortgage balance and origination-year inputs are unchanged. API callers that pass either removed input now receive an unknown-variable error and should supply home_mortgage_interest on the filer(s) instead. Datasets that still carry the removed columns load with policyengine-core's unknown-column warning, and any tax unit whose interest appeared only in those columns now computes zero mortgage interest; the certified default dataset has no such tax units. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml index dc32eb9c434..e6030e9b0ab 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml @@ -237,7 +237,6 @@ - head - spouse first_home_mortgage_balance: 1000000.0 - first_home_mortgage_interest: 2743.0 takes_up_aca_if_eligible: true takes_up_dc_ptc: true takes_up_eitc: true @@ -587,7 +586,6 @@ members: &id007 - head first_home_mortgage_balance: 273000.0 - first_home_mortgage_interest: 7566.0 takes_up_aca_if_eligible: true takes_up_dc_ptc: true takes_up_eitc: true @@ -660,9 +658,7 @@ members: &id008 - head first_home_mortgage_balance: 794797.6875 - first_home_mortgage_interest: 2975.722412109375 second_home_mortgage_balance: 205202.3125 - second_home_mortgage_interest: 768.277587890625 takes_up_aca_if_eligible: true takes_up_dc_ptc: true takes_up_eitc: true