From 8fc6faaaf5d9a25db625b12ff5aa9becf5b1d995 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 11:56:08 -0400 Subject: [PATCH 01/10] Gate New York itemizing on federal itemizing only before 2018 N.Y. Tax Law 615(a) before 2018 let only federal itemizers take the New York itemized deduction (2017 IT-201-I line 34); from 2018 a filer may take either New York deduction whatever the federal choice (2018 IT-196-I). Main's ny_deductions took the larger deduction in every year, and ny_itemizes required federal itemizing in every year. Both now read one dated parameter, and ny_deductions follows ny_itemizes. Co-Authored-By: Claude Opus 5.5 --- ...ny-itemized-election-federal-gate.fixed.md | 1 + .../federal_itemization_required.yaml | 15 +++++++++ .../deductions/itemized/ny_itemizes.yaml | 32 ++++++++++++++++++- .../deductions/itemized/ny_itemizes.py | 16 +++++++--- .../income/taxable_income/ny_deductions.py | 11 +++++-- 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 changelog.d/ny-itemized-election-federal-gate.fixed.md create mode 100644 policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml diff --git a/changelog.d/ny-itemized-election-federal-gate.fixed.md b/changelog.d/ny-itemized-election-federal-gate.fixed.md new file mode 100644 index 00000000000..c6abe75df86 --- /dev/null +++ b/changelog.d/ny-itemized-election-federal-gate.fixed.md @@ -0,0 +1 @@ +Require federal itemizing for New York itemized deductions only before 2018, and apply it to the deduction New York uses. diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml new file mode 100644 index 00000000000..d1ac64f2297 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml @@ -0,0 +1,15 @@ +description: New York allows the New York itemized deduction only to filers who itemize on their federal return, if this is true. +values: + 2017-01-01: true + 2018-01-01: false +metadata: + unit: bool + period: year + label: New York itemized deduction requires federal itemization + reference: + - title: N.Y. Tax Law 615(a) + href: https://legislation.nysenate.gov/pdf/laws/TAX615 + - title: 2017 Form IT-201 instructions, line 34 + href: https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20 + - title: 2018 Form IT-196 instructions, general information + href: https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml index 7bb743a7863..e1a59d0ceb2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml @@ -20,9 +20,26 @@ output: ny_itemizes: false -- name: Case 3, not itemzies. +- name: Case 3, not itemizing federally does not stop New York itemizing from 2018. period: 2021 absolute_error_margin: 0 + input: + tax_unit_itemizes: false + ny_standard_deduction: 300 + ny_itemized_deductions: 500 + state_code: NY + output: + # 2018 IT-196 instructions: "you can choose to itemize your deductions for + # New York State income tax purposes whether or not you itemized your + # deductions on your federal income tax return." + ny_itemizes: true + +# N.Y. Tax Law 615(a) before 2018; 2017 IT-201 instructions, line 34: "If you +# took the standard deduction on your federal return ... you must take the New +# York standard deduction." +- name: Case 4, 2017 federal standard deduction filer takes the New York standard deduction. + period: 2017 + absolute_error_margin: 0 input: tax_unit_itemizes: false ny_standard_deduction: 300 @@ -30,3 +47,16 @@ state_code: NY output: ny_itemizes: false + ny_deductions: 300 + +- name: Case 5, 2017 federal itemizer takes the larger New York itemized deduction. + period: 2017 + absolute_error_margin: 0 + input: + tax_unit_itemizes: true + ny_standard_deduction: 300 + ny_itemized_deductions: 500 + state_code: NY + output: + ny_itemizes: true + ny_deductions: 500 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py index 8d56ce1f45b..2c1ad7c19e4 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py @@ -5,13 +5,21 @@ class ny_itemizes(Variable): value_type = bool entity = TaxUnit label = "Itemizes New York deductions" - documentation = "Tax units who itemize their federal deductions can opt to itemize their New York deductions. However, if a standard deduction causes a lower tax liability, they must choose that." + documentation = "Whether the tax unit takes the New York itemized deduction, which it does when that is at least the New York standard deduction. Before 2018 only federal itemizers could itemize for New York." definition_period = YEAR defined_for = StateCode.NY - reference = "https://www.nysenate.gov/legislation/laws/TAX/613" + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/613", + "https://legislation.nysenate.gov/pdf/laws/TAX615", + "https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20", + "https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1", + ) def formula(tax_unit, period, parameters): - federal_itemizes = tax_unit("tax_unit_itemizes", period) + p = parameters(period).gov.states.ny.tax.income.deductions.itemized + may_itemize = True + if p.federal_itemization_required: + may_itemize = tax_unit("tax_unit_itemizes", period) standard_deduction = tax_unit("ny_standard_deduction", period) itemized_deductions = tax_unit("ny_itemized_deductions", period) - return federal_itemizes & (itemized_deductions >= standard_deduction) + return may_itemize & (itemized_deductions >= standard_deduction) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py index 84554be017e..26279d06cf1 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py @@ -7,11 +7,18 @@ class ny_deductions(Variable): label = "NY income deductions" unit = USD definition_period = YEAR - reference = "https://www.nysenate.gov/legislation/laws/TAX/613" + reference = ( + "https://www.nysenate.gov/legislation/laws/TAX/613", + "https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20", + ) defined_for = StateCode.NY def formula(tax_unit, period, parameters): - return max_( + # ny_itemizes picks the larger deduction among those the filer may + # take: before 2018 a federal standard deduction filer must take the + # New York standard deduction (N.Y. Tax Law 615(a)). + return where( + tax_unit("ny_itemizes", period), tax_unit("ny_itemized_deductions", period), tax_unit("ny_standard_deduction", period), ) From 9df7ec2240f294b09da76a5f1801c30f03eb62f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 21:39:11 +0000 Subject: [PATCH 02/10] Cite the 2025 IT-201 line 34 instructions for New York's itemizing election The current instructions let a filer take either New York deduction with no federal itemizing condition, which supports the parameter's value from 2018. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6Fmi4DmuA9GyXnRKdV --- .../deductions/itemized/federal_itemization_required.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml index d1ac64f2297..febe0b03aae 100644 --- a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml @@ -13,3 +13,5 @@ metadata: href: https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20 - title: 2018 Form IT-196 instructions, general information href: https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1 + - title: 2025 Form IT-201 instructions, line 34 + href: https://www.tax.ny.gov/forms/html-instructions/2025/it/it201i-2025.htm#step-5 From 9fcaf26edfcc6c3dbbc5eb7e03964f675c1920ff Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 00:05:02 +0000 Subject: [PATCH 03/10] Choose New York's itemizing election by State plus City tax Picking the larger deduction made a federal standard-deduction filer who pays tuition itemize for New York from 2018 and lose the college tuition credit, even when the standard deduction plus the credit costs less. ny_itemizes now compares ny_income_tax plus nyc_income_tax in branches where it is an input, and itemizes only when that is strictly cheaper. Before 2018 the federal gate reads tax_unit_itemizes from the branch where New York itemizes: the federal SALT deduction adds New York City tax, which follows this election, so reading it in the default branch was circular (every 2017 New York City household raised CycleError). Cases 1-5 now carry income so the comparison is not a zero-tax tie; new cases cover the tuition witness, a New York City resident whose choice turns on City tax, and a tie. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6Hor7QXUMxRAcCPhrB --- ...ny-itemized-election-federal-gate.fixed.md | 2 +- .../federal_itemization_required.yaml | 6 +- .../deductions/itemized/ny_itemizes.yaml | 175 ++++++++++++++++-- .../deductions/itemized/ny_itemizes.py | 69 ++++++- .../income/taxable_income/ny_deductions.py | 6 +- 5 files changed, 225 insertions(+), 33 deletions(-) diff --git a/changelog.d/ny-itemized-election-federal-gate.fixed.md b/changelog.d/ny-itemized-election-federal-gate.fixed.md index c6abe75df86..75e8a7a90ab 100644 --- a/changelog.d/ny-itemized-election-federal-gate.fixed.md +++ b/changelog.d/ny-itemized-election-federal-gate.fixed.md @@ -1 +1 @@ -Require federal itemizing for New York itemized deductions only before 2018, and apply it to the deduction New York uses. +Choose New York's itemized-or-standard deduction election by comparing New York State plus New York City income tax, so filers who pay college tuition keep the standard deduction and tuition credit when that costs less, and require federal itemizing for the New York itemized deduction only before 2018. diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml index febe0b03aae..4ed42bfd2d8 100644 --- a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml @@ -7,11 +7,11 @@ metadata: period: year label: New York itemized deduction requires federal itemization reference: - - title: N.Y. Tax Law 615(a) + - title: N.Y. Tax Law 615(a), current text (supports the 2018 value) href: https://legislation.nysenate.gov/pdf/laws/TAX615 - - title: 2017 Form IT-201 instructions, line 34 + - title: 2017 Form IT-201 instructions, line 34 (supports the 2017 value) href: https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20 - - title: 2018 Form IT-196 instructions, general information + - title: 2018 Form IT-196 instructions, New for 2018 (p. 1) href: https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1 - title: 2025 Form IT-201 instructions, line 34 href: https://www.tax.ny.gov/forms/html-instructions/2025/it/it201i-2025.htm#step-5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml index e1a59d0ceb2..63622044acd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml @@ -1,62 +1,197 @@ -- name: Case 1, itemzied deduction higher than standard deduction. +# New York's itemizing election is chosen by tax: among filers who may +# itemize, itemize only when that gives strictly less New York State plus +# New York City income tax. +# +# Hand values. 2021 single brackets: 4% to $8,500 ($340), 4.5% to $11,700 +# ($144), 5.25% to $13,900 ($115.50), 5.9% to $21,400 ($442.50), then 5.97%; +# tax at $21,400 is $1,042. From 2023 the 5.9% bracket is 5.5% and runs to +# $80,650; tax at $13,900 is $599.50. A single filer with $50,000 or $40,000 +# of wages gets no New York household credit or EITC. + +- name: Case 1, a federal itemizer itemizes for New York when its itemized deduction is larger. period: 2021 - absolute_error_margin: 0 + absolute_error_margin: 0.01 input: tax_unit_itemizes: true - ny_standard_deduction: 200 - ny_itemized_deductions: 300 + employment_income: 50_000 + ny_standard_deduction: 8_000 + ny_itemized_deductions: 10_000 state_code: NY output: + # Itemize: taxable income 50,000 - 10,000 = 40,000; + # tax 1,042 + 5.97% x (40,000 - 21,400) = 1,042 + 1,110.42 = 2,152.42. + # Standard: taxable income 42,000; + # tax 1,042 + 5.97% x 20,600 = 2,271.82. ny_itemizes: true + ny_deductions: 10_000 + ny_income_tax: 2_152.42 -- name: Case 2, itemzied deduction less than standard deduction. +- name: Case 2, a federal itemizer takes the New York standard deduction when its itemized deduction is smaller. period: 2021 - absolute_error_margin: 0 + absolute_error_margin: 0.01 input: tax_unit_itemizes: true - ny_standard_deduction: 300 - ny_itemized_deductions: 100 + employment_income: 50_000 + ny_standard_deduction: 8_000 + ny_itemized_deductions: 6_000 state_code: NY output: + # Itemize: taxable income 44,000; tax 1,042 + 5.97% x 22,600 = 2,391.22. + # Standard: taxable income 42,000; tax 2,271.82, the lower. ny_itemizes: false + ny_deductions: 8_000 + ny_income_tax: 2_271.82 - name: Case 3, not itemizing federally does not stop New York itemizing from 2018. period: 2021 - absolute_error_margin: 0 + absolute_error_margin: 0.01 input: tax_unit_itemizes: false - ny_standard_deduction: 300 - ny_itemized_deductions: 500 + employment_income: 50_000 + ny_standard_deduction: 8_000 + ny_itemized_deductions: 10_000 state_code: NY output: - # 2018 IT-196 instructions: "you can choose to itemize your deductions for - # New York State income tax purposes whether or not you itemized your - # deductions on your federal income tax return." + # 2018 Form IT-196 instructions, New for 2018 (p. 1): "you can choose to + # itemize your deductions for New York State income tax purposes whether + # or not you itemized your deductions on your federal income tax return." + # https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1 + # Itemizing gives 2,152.42 and the standard deduction 2,271.82, as in + # Case 1. ny_itemizes: true + ny_deductions: 10_000 + ny_income_tax: 2_152.42 # N.Y. Tax Law 615(a) before 2018; 2017 IT-201 instructions, line 34: "If you # took the standard deduction on your federal return ... you must take the New # York standard deduction." +# The 2017 cases check the election and deductions. The model's 2017 New York +# tax uses its earliest rate schedule (2021); with positive rates the larger +# deduction gives the lower tax under any schedule. - name: Case 4, 2017 federal standard deduction filer takes the New York standard deduction. period: 2017 absolute_error_margin: 0 input: tax_unit_itemizes: false - ny_standard_deduction: 300 - ny_itemized_deductions: 500 + employment_income: 50_000 + ny_standard_deduction: 8_000 + ny_itemized_deductions: 10_000 state_code: NY output: + # Itemizing would give taxable income 40,000 rather than 42,000 and so + # less tax, but the federal standard deduction rules it out. ny_itemizes: false - ny_deductions: 300 + ny_deductions: 8_000 + ny_taxable_income: 42_000 - name: Case 5, 2017 federal itemizer takes the larger New York itemized deduction. period: 2017 absolute_error_margin: 0 input: tax_unit_itemizes: true - ny_standard_deduction: 300 - ny_itemized_deductions: 500 + employment_income: 50_000 + ny_standard_deduction: 8_000 + ny_itemized_deductions: 10_000 state_code: NY output: + # Taxable income 40,000 itemizing against 42,000 with the standard + # deduction, so itemizing gives less tax. + ny_itemizes: true + ny_deductions: 10_000 + ny_taxable_income: 40_000 + +- name: Case 6, a federal standard deduction filer keeps the standard deduction and the college tuition credit when that costs less. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 40_000 + qualified_tuition_expenses: 10_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + output: + # Federal: the $14,600 standard deduction exceeds any itemized deduction. + # The New York itemized deduction is the $10,000 college tuition + # deduction (100% of $10,000); the standard deduction is $8,000. + # Itemize: taxable income 30,000; tax 599.50 + 5.5% x (30,000 - 13,900) + # = 599.50 + 885.50 = 1,485, with no tuition credit. + # Standard: taxable income 32,000; tax 599.50 + 5.5% x 18,100 = 1,595, + # less the credit 100% x 200 + 4% x (10,000 - 5,000) = 400: 1,195. + # 1,195 < 1,485, so the standard deduction and the credit. + ny_itemizes: false + ny_deductions: 8_000 + ny_college_tuition_credit: 400 + ny_income_tax: 1_195 + +- name: Case 7, a New York City resident itemizes because the city tax saved outweighs the State credit given up. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 50_000 + qualified_tuition_expenses: 10_000 + real_estate_taxes: 4_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + in_nyc: true + output: + # New York itemized deduction: 10,000 tuition + 4,000 property tax + # = 14,000; standard 8,000. + # State. Itemize: taxable income 36,000; tax 599.50 + 5.5% x 22,100 + # = 1,815, no credit. Standard: taxable income 42,000; tax + # 599.50 + 5.5% x 28,100 = 2,145, less the 400 credit = 1,745. + # State tax alone favours the standard deduction, by 70. + # City (single: 3.078% to 12,000, 3.762% to 25,000, 3.819% to 50,000; + # school tax credit 63 fixed plus 0.171% of the first 12,000 and + # 0.228% above). + # Itemize: 369.36 + 489.06 + 3.819% x 11,000 = 1,278.51, less + # 63 + 20.52 + 0.228% x 24,000 = 138.24: 1,140.27. + # Standard: 369.36 + 489.06 + 3.819% x 17,000 = 1,507.65, less + # 63 + 20.52 + 0.228% x 30,000 = 151.92: 1,355.73. + # Total: itemize 1,815 + 1,140.27 = 2,955.27; standard 1,745 + 1,355.73 + # = 3,100.73. Itemizing saves 145.46. ny_itemizes: true - ny_deductions: 500 + ny_deductions: 14_000 + ny_college_tuition_credit: 0 + ny_income_tax: 1_815 + nyc_taxable_income: 36_000 + nyc_income_tax: 1_140.27 + +- name: Case 8, a tie in New York tax takes the standard deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 40_000 + real_estate_taxes: 8_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + output: + # The New York itemized deduction (8,000 property tax) equals the + # standard deduction, so taxable income is 32,000 either way and the tax + # is 599.50 + 5.5% x 18,100 = 1,595 under both choices. + ny_itemized_deductions: 8_000 + ny_itemizes: false + ny_deductions: 8_000 + ny_income_tax: 1_595 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py index 2c1ad7c19e4..e907ba5b4a6 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py @@ -1,25 +1,82 @@ from policyengine_us.model_api import * +def get_ny_itemization_branch(tax_unit, period, itemizes: bool): + """ + The branch of ``tax_unit``'s simulation that fixes ``ny_itemizes``. + + With the election an input, the branch calculates New York taxes without + calling the ``ny_itemizes`` formula that asks for them. + """ + return get_override_branch( + tax_unit.simulation, + "ny_itemizing" if itemizes else "ny_not_itemizing", + period, + {"ny_itemizes": np.full(tax_unit.count, itemizes, dtype=bool)}, + ) + + class ny_itemizes(Variable): value_type = bool entity = TaxUnit label = "Itemizes New York deductions" - documentation = "Whether the tax unit takes the New York itemized deduction, which it does when that is at least the New York standard deduction. Before 2018 only federal itemizers could itemize for New York." + documentation = ( + "Whether the tax unit takes the New York itemized deduction rather " + "than the New York standard deduction. Among filers who may itemize " + "(before 2018, only federal itemizers), it itemizes when that gives " + "less New York State plus New York City income tax, counting the " + "college tuition credit given up by itemizing. A tie takes the " + "standard deduction." + ) definition_period = YEAR defined_for = StateCode.NY reference = ( "https://www.nysenate.gov/legislation/laws/TAX/613", + # N.Y. Tax Law 615(a), current text (supports the 2018 rule). "https://legislation.nysenate.gov/pdf/laws/TAX615", + # 2017 Form IT-201 instructions, line 34 (supports the 2017 rule). "https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20", + # 2018 Form IT-196 instructions, New for 2018 (p. 1). "https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1", + # 2025 Form IT-201 instructions, line 34, whose note gives the choice + # of a college tuition credit or itemized deduction. + "https://www.tax.ny.gov/forms/html-instructions/2025/it/it201i-2025.htm#step-5", ) def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ny.tax.income.deductions.itemized - may_itemize = True + # Values both choices share, calculated before branching so that + # both branches inherit them rather than recalculate them. Federal + # itemizing is not among them: the federal deduction for state and + # local taxes adds New York City income tax, which follows this + # election, so the federal choice is not calculated here. + tax_unit("ny_agi", period) + tax_unit("ny_standard_deduction", period) + tax_unit("ny_itemized_deductions", period) + tax_unit("ny_allowable_college_tuition_expenses", period) + itemizing = get_ny_itemization_branch(tax_unit, period, itemizes=True) if p.federal_itemization_required: - may_itemize = tax_unit("tax_unit_itemizes", period) - standard_deduction = tax_unit("ny_standard_deduction", period) - itemized_deductions = tax_unit("ny_itemized_deductions", period) - return may_itemize & (itemized_deductions >= standard_deduction) + # Before 2018, N.Y. Tax Law 615(a) lets only a filer who itemized + # federally elect the New York itemized deduction. The federal + # choice is the one made when New York itemizes, as it is taken + # from the branch where New York itemizes. + may_itemize = itemizing.calculate("tax_unit_itemizes", period) + if not may_itemize.any(): + return may_itemize + else: + may_itemize = np.ones(tax_unit.count, dtype=bool) + not_itemizing = get_ny_itemization_branch(tax_unit, period, itemizes=False) + # The election moves New York taxable income, which New York City + # taxable income adds, and it decides the college tuition credit, so + # the choices are compared on State plus City tax. The Yonkers + # resident surcharge is a fixed share of State tax, so it cannot + # change which choice costs less. + taxes = ["ny_income_tax", "nyc_income_tax"] + tax_if_itemizing = sum(itemizing.calculate(tax, period) for tax in taxes) + tax_if_not_itemizing = sum( + not_itemizing.calculate(tax, period) for tax in taxes + ) + # Itemize only when it saves more than a rounding error: a tie takes + # the standard deduction, which keeps the college tuition credit. + TOLERANCE = 0.01 + return may_itemize & (tax_if_itemizing < tax_if_not_itemizing - TOLERANCE) diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py index 26279d06cf1..2f1df460bf7 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py @@ -14,9 +14,9 @@ class ny_deductions(Variable): defined_for = StateCode.NY def formula(tax_unit, period, parameters): - # ny_itemizes picks the larger deduction among those the filer may - # take: before 2018 a federal standard deduction filer must take the - # New York standard deduction (N.Y. Tax Law 615(a)). + # ny_itemizes makes the election: it itemizes when that gives less + # New York State plus City tax, and before 2018 only for a federal + # itemizer (N.Y. Tax Law 615(a)). return where( tax_unit("ny_itemizes", period), tax_unit("ny_itemized_deductions", period), From 39c29003dac09191532e4841b3a24cecc12f1d79 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 15:26:05 +0000 Subject: [PATCH 04/10] Pin the 2017 cycle fix with a calculated federal choice Every 2017 case set tax_unit_itemizes as an input, so a mutant that read the federal choice in the default branch again passed the whole file while raising CycleError for every 2017 New York City household. Case 9 calculates the federal choice for a 2017 New York City resident. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QpcqEC8X8Zh3TKE8oN297V --- .../deductions/itemized/ny_itemizes.yaml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml index 63622044acd..3dc2a8e702c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml @@ -195,3 +195,30 @@ ny_itemizes: false ny_deductions: 8_000 ny_income_tax: 1_595 + +- name: Case 9, a 2017 New York City resident whose federal choice is calculated, not given. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 50_000 + real_estate_taxes: 4_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + in_nyc: true + output: + # Federal SALT adds New York City tax, which follows this election, so + # the federal choice must come from a branch (else CycleError). + # New York itemized 4,000 < standard 8,000: taxable income 42,000. + # City: 369.36 + 489.06 + 3.819% x 17,000 = 1,507.65, less + # 63 + 20.52 + 0.228% x 30,000 = 151.92: 1,355.73. + ny_itemizes: false + ny_deductions: 8_000 + nyc_income_tax: 1_355.73 From 3f7cc6e8516a3e199b8d3a8400eb3fec8ecf90b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 16:23:48 +0000 Subject: [PATCH 05/10] Compare only the State and City tax the NY itemizing election moves Comparing ny_income_tax + nyc_income_tax in the two branches brought in credits such as the Empire State child credit, which recalculate federal tax inside each branch, and made a New York City household with a child about 11 times slower. The branches now calculate tax before refundable credits (the household, solar and geothermal credits are capped at tax before credits), less the credits that follow the election: the college tuition credit, the New York EITC and supplemental EITC where the household credit allowed moves, the City school tax credit and the City income tax elimination credit. A filer without college tuition expenses gives up no credit by itemizing, so the larger deduction gives no more tax: when no filer has both tuition expenses and a larger itemized deduction the formula skips the branches, and otherwise such filers still take the larger deduction. Cases 10 to 15 pin each part of the comparison and the fallback. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QpcqEC8X8Zh3TKE8oN297V --- .../deductions/itemized/ny_itemizes.yaml | 240 ++++++++++++++++++ .../deductions/itemized/ny_itemizes.py | 90 +++++-- 2 files changed, 312 insertions(+), 18 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml index 3dc2a8e702c..7b2e83e2e6f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml @@ -222,3 +222,243 @@ ny_itemizes: false ny_deductions: 8_000 nyc_income_tax: 1_355.73 + +# Cases 10 to 15 pin each part of the tax the election compares: tax before +# refundable credits (the non-refundable household credit is capped at tax +# before credits), less the college tuition credit, the New York EITC where +# the election moves the household credit allowed, the City school tax credit +# and the City income tax elimination credit. +# 2024 single brackets as above. Federal EITC 2024: no child, 7.65% phase-out +# from $10,330 ($17,250 joint) of a $632 maximum; one child, $4,213 maximum. +# New York EITC: 30% of the federal credit, less the household credit allowed. +- name: Case 10, a tie in tax with college tuition expenses takes the standard deduction. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 16_000 + qualified_tuition_expenses: 5_000 + deductible_mortgage_interest: 8_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + output: + # Itemized 8,000 interest + 5,000 tuition = 13,000; standard 8,000. + # Household credit (single, AGI 16,000): 45 either way. + # EITC: federal 632 - 7.65% x (16,000 - 10,330) = 198.245; + # New York 30% x 198.245 - 45 = 14.4735 either way. + # Itemize: taxable income 3,000; 4% = 120, less 45 = 75; less the EITC: + # 60.5265. + # Standard: taxable income 8,000; 4% = 320, less 45 = 275; less the + # tuition credit (100% of the first 200) and the EITC: 60.5265. A tie. + ny_itemizes: false + ny_deductions: 8_000 + ny_college_tuition_credit: 200 + ny_income_tax: 60.53 + +- name: Case 11, the household credit is capped at tax, so tax before refundable credits is compared. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 20_000 + qualified_tuition_expenses: 2_000 + deductible_mortgage_interest: 20_000 + person2: + age: 40 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NY + output: + # Joint. Itemized 20,000 + 2,000 = 22,000; standard 16,050. + # EITC: federal 632 - 7.65% x (20,000 - 17,250) = 421.625; New York + # 30% = 126.4875, less the household credit allowed. + # Standard: taxable income 3,950; 4% = 158; household credit 75 (joint, + # AGI 20,000), so 83; less the tuition credit 200 and the EITC + # 126.4875 - 75 = 51.4875: -168.4875. + # Itemize: taxable income 0, so no tax and no household credit allowed; + # EITC 126.4875: -126.4875. The standard deduction costs 42 less. + # Tax before credits (158) in place of 83 would make the standard + # deduction -93.4875 and pick itemizing. + ny_itemizes: false + ny_deductions: 16_050 + ny_household_credit: 75 + ny_eitc: 51.49 + ny_income_tax: -168.49 + +- name: Case 12, the New York EITC follows the household credit allowed, which itemizing removes. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 18_000 + qualified_tuition_expenses: 2_000 + deductible_mortgage_interest: 16_000 + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1] + child_marital_unit: + members: [person2] + households: + household: + members: [person1, person2] + state_code: NY + output: + # Head of household with one child. Itemized 16,000 + 2,000 = 18,000; + # standard 11,200; dependent exemption 1,000. + # EITC: New York 30% x 4,213 = 1,263.90, less the household credit + # allowed. Empire State child credit 330 either way. + # Standard: taxable income 5,800; 4% = 232; household credit 75, so + # 157; less the tuition credit 200, the EITC 1,263.90 - 75 = 1,188.90 + # and 330: -1,561.90. + # Itemize: taxable income 0, so no household credit allowed; EITC + # 1,263.90; less 330: -1,593.90. Itemizing costs 32 less. + # Without the EITC the comparison would find the standard deduction 43 + # cheaper (157 - 200 against 0). + ny_itemizes: true + ny_deductions: 18_000 + ny_eitc: 1_263.90 + ny_income_tax: -1_593.90 + +- name: Case 13, a New York City resident keeps the standard deduction once the school tax credit is counted. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 50_000 + qualified_tuition_expenses: 2_000 + real_estate_taxes: 8_175 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: NY + in_nyc: true + output: + # Itemized 8,175 + 2,000 = 10,175; standard 8,000. + # State. Itemize: taxable income 39,825; 599.50 + 5.5% x 25,925 + # = 2,025.375. Standard: taxable income 42,000; 2,145, less the + # tuition credit 200 = 1,945. + # City (as in Case 7). Itemize: 369.36 + 489.06 + 3.819% x 14,825 + # = 1,424.58675, less the school tax credit 63 + 20.52 + 0.228% x + # 27,825 = 146.961: 1,277.62575. Standard: 1,507.65 - 151.92 + # = 1,355.73. + # Total: itemize 3,303.00; standard 3,300.73, which is 2.27 less. + # Without the school tax credit: itemize 3,449.96 against 3,452.65. + ny_itemizes: false + ny_deductions: 8_000 + ny_income_tax: 1_945 + nyc_income_tax: 1_355.73 + +- name: Case 14, the City income tax elimination credit leaves a 2025 New York City family no City saving from itemizing. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 30_000 + qualified_tuition_expenses: 10_000 + deductible_mortgage_interest: 7_000 + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1] + child_marital_unit: + members: [person2] + households: + household: + members: [person1, person2] + state_code: NY + in_nyc: true + output: + # Head of household with one child. Itemized 7,000 + 10,000 = 17,000; + # standard 11,200; dependent exemption 1,000. The New York EITC + # (954.5991), the Empire State child credit (330), the household credit + # (25) and the City EITC (489.7995) are the same either way. + # State (4% to 12,800, 4.5% to 17,650, 5.25% to 20,900). Standard: + # taxable income 17,800; 512 + 218.25 + 7.875 = 738.125, less 25 and + # the tuition credit 200 + 4% x 5,000 = 400: 313.125. Itemize: taxable + # income 12,000; 480 - 25 = 455. + # City (head of household: 3.078% to 14,400, then 3.762%). Standard: + # 443.232 + 3.762% x 3,400 = 571.14; school tax credit 63 + 0.171% x + # 14,400 + 0.228% x 3,400 = 95.376. Itemize: 3.078% x 12,000 = 369.36; + # school tax credit 63 + 20.52 = 83.52. + # Elimination credit (federal AGI 30,000, under the 31,503 threshold for + # one dependent): City tax after the City EITC. Standard: 571.14 - + # 489.7995 = 81.3405. Itemize: 369.36 < 489.7995, so 0. + # City tax: standard 571.14 - 489.7995 - 95.376 - 81.3405 = -95.376; + # itemize 369.36 - 489.7995 - 83.52 = -203.9595. + # State tax: standard 313.125 - 954.5991 - 330 = -971.4741; itemize + # 455 - 954.5991 - 330 = -829.5991. + # Total: standard -1,066.85; itemize -1,033.56. The standard deduction + # costs 33.29 less; without the elimination credit itemizing would + # look 48.05 cheaper. + ny_itemizes: false + ny_college_tuition_credit: 400 + nyc_income_tax_elimination_credit: 81.34 + ny_income_tax: -971.47 + nyc_income_tax: -95.38 + +- name: Case 15, without tuition expenses the larger itemized deduction is taken, even beside a filer whose choice needs the taxes. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 40_000 + qualified_tuition_expenses: 10_000 + person2: + age: 30 + employment_income: 5_000 + real_estate_taxes: 10_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household1: + members: [person1] + state_code: NY + household2: + members: [person2] + state_code: NY + output: + # The first filer is Case 6: the standard deduction and the credit. + # The second has no tuition expenses, so itemizing gives up no credit: + # itemized 10,000 > standard 8,000, and taxable income is 0 either way, + # so the larger deduction is taken although the tax ties. + ny_itemizes: [false, true] + ny_deductions: [8_000, 10_000] diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py index e907ba5b4a6..3d18e6c7f2a 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py @@ -1,5 +1,28 @@ from policyengine_us.model_api import * +# The State and City tax the election can move. Tax before refundable +# credits is tax before credits less the non-refundable credits, which tax +# before credits caps. +TAXES_THE_ELECTION_MOVES = [ + "ny_income_tax_before_refundable_credits", + "nyc_income_tax_before_refundable_credits", +] +# The refundable credits that follow the election. +CREDITS_THE_ELECTION_MOVES = [ + # Itemizing gives up the college tuition credit. + "ny_college_tuition_credit", + # The school tax credit's rate reduction amount is a schedule of City + # taxable income. + "nyc_school_tax_credit", + # The City income tax elimination credit is a share of City tax before + # refundable credits, less the City EITC and child care credit. + "nyc_income_tax_elimination_credit", +] +# The New York EITC is reduced by the household credit allowed, which tax +# before credits caps, and the supplemental EITC is a share of the EITC. The +# election moves them only where it moves the household credit allowed. +EITCS_THE_ELECTION_MOVES = ["ny_eitc", "ny_supplemental_eitc"] + def get_ny_itemization_branch(tax_unit, period, itemizes: bool): """ @@ -16,6 +39,19 @@ def get_ny_itemization_branch(tax_unit, period, itemizes: bool): ) +def tax_the_election_moves(branch, period, credits): + """ + The New York State and City tax in ``branch`` that the election can move: + tax before refundable credits, less ``credits``. + + It leaves out the credits the election moves only through federal tax, + such as the Empire State child credit, so the branch does not + recalculate federal tax. + """ + tax = sum(branch.calculate(name, period) for name in TAXES_THE_ELECTION_MOVES) + return tax - sum(branch.calculate(name, period) for name in credits) + + class ny_itemizes(Variable): value_type = bool entity = TaxUnit @@ -23,10 +59,12 @@ class ny_itemizes(Variable): documentation = ( "Whether the tax unit takes the New York itemized deduction rather " "than the New York standard deduction. Among filers who may itemize " - "(before 2018, only federal itemizers), it itemizes when that gives " - "less New York State plus New York City income tax, counting the " - "college tuition credit given up by itemizing. A tie takes the " - "standard deduction." + "(before 2018, only federal itemizers), a filer with college tuition " + "expenses itemizes when that gives less New York State plus New York " + "City income tax, counting the college tuition credit given up by " + "itemizing, and a tie takes the standard deduction. A filer without " + "them gives up no credit by itemizing, and itemizes when the " + "itemized deduction is larger." ) definition_period = YEAR defined_for = StateCode.NY @@ -36,7 +74,7 @@ class ny_itemizes(Variable): "https://legislation.nysenate.gov/pdf/laws/TAX615", # 2017 Form IT-201 instructions, line 34 (supports the 2017 rule). "https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20", - # 2018 Form IT-196 instructions, New for 2018 (p. 1). + # 2018 Form IT-196 instructions, New for 2018. "https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1", # 2025 Form IT-201 instructions, line 34, whose note gives the choice # of a college tuition credit or itemized deduction. @@ -51,32 +89,48 @@ def formula(tax_unit, period, parameters): # local taxes adds New York City income tax, which follows this # election, so the federal choice is not calculated here. tax_unit("ny_agi", period) - tax_unit("ny_standard_deduction", period) - tax_unit("ny_itemized_deductions", period) - tax_unit("ny_allowable_college_tuition_expenses", period) - itemizing = get_ny_itemization_branch(tax_unit, period, itemizes=True) + standard = tax_unit("ny_standard_deduction", period) + itemized = tax_unit("ny_itemized_deductions", period) + tuition = tax_unit("ny_allowable_college_tuition_expenses", period) if p.federal_itemization_required: # Before 2018, N.Y. Tax Law 615(a) lets only a filer who itemized # federally elect the New York itemized deduction. The federal # choice is the one made when New York itemizes, as it is taken # from the branch where New York itemizes. + itemizing = get_ny_itemization_branch(tax_unit, period, itemizes=True) may_itemize = itemizing.calculate("tax_unit_itemizes", period) if not may_itemize.any(): return may_itemize else: may_itemize = np.ones(tax_unit.count, dtype=bool) + larger = itemized > standard + # Without college tuition expenses, itemizing gives up no credit, so + # the larger deduction gives no more tax. Only a filer with both a + # larger itemized deduction and tuition expenses needs the taxes. + trades_credit = may_itemize & larger & (tuition > 0) + if not trades_credit.any(): + return may_itemize & larger + itemizing = get_ny_itemization_branch(tax_unit, period, itemizes=True) not_itemizing = get_ny_itemization_branch(tax_unit, period, itemizes=False) # The election moves New York taxable income, which New York City # taxable income adds, and it decides the college tuition credit, so - # the choices are compared on State plus City tax. The Yonkers - # resident surcharge is a fixed share of State tax, so it cannot - # change which choice costs less. - taxes = ["ny_income_tax", "nyc_income_tax"] - tax_if_itemizing = sum(itemizing.calculate(tax, period) for tax in taxes) - tax_if_not_itemizing = sum( - not_itemizing.calculate(tax, period) for tax in taxes - ) + # the choices are compared on the State plus City tax they move. The + # Yonkers resident surcharge is a fixed share of State tax, so it + # cannot change which choice costs less. + credits = list(CREDITS_THE_ELECTION_MOVES) + # Where the household credit allowed is the same under both choices, + # so is the New York EITC, and leaving it out of both sides spares + # the branches its federal credit (in 2021, recalculated under the + # law before the American Rescue Plan Act). + household_credit_moves = itemizing.calculate( + "ny_household_credit", period + ) != not_itemizing.calculate("ny_household_credit", period) + if (trades_credit & household_credit_moves).any(): + credits += EITCS_THE_ELECTION_MOVES + tax_if_itemizing = tax_the_election_moves(itemizing, period, credits) + tax_if_not_itemizing = tax_the_election_moves(not_itemizing, period, credits) # Itemize only when it saves more than a rounding error: a tie takes # the standard deduction, which keeps the college tuition credit. TOLERANCE = 0.01 - return may_itemize & (tax_if_itemizing < tax_if_not_itemizing - TOLERANCE) + lower = tax_if_itemizing < tax_if_not_itemizing - TOLERANCE + return may_itemize & where(trades_credit, lower, larger) From 6dc54d679d5690e56c603c64f3347f4c40645d37 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 16:23:48 +0000 Subject: [PATCH 06/10] Drop the page from the single-page IT-196 reference title CLAUDE.md puts a single cited page in the href only, not the title. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QpcqEC8X8Zh3TKE8oN297V --- .../deductions/itemized/federal_itemization_required.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml index 4ed42bfd2d8..841184a6fdb 100644 --- a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml @@ -11,7 +11,7 @@ metadata: href: https://legislation.nysenate.gov/pdf/laws/TAX615 - title: 2017 Form IT-201 instructions, line 34 (supports the 2017 value) href: https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20 - - title: 2018 Form IT-196 instructions, New for 2018 (p. 1) + - title: 2018 Form IT-196 instructions, New for 2018 href: https://www.tax.ny.gov/pdf/2018/inc/it196i_2018.pdf#page=1 - title: 2025 Form IT-201 instructions, line 34 href: https://www.tax.ny.gov/forms/html-instructions/2025/it/it201i-2025.htm#step-5 From ea021258068294efdd946637fcf3d37ab2587d74 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 16:38:15 +0000 Subject: [PATCH 07/10] Check the NY itemizing election against all of State and City tax The election now compares only the tax it can move, so a Hypothesis test checks the choice against the whole of ny_income_tax + nyc_income_tax: for 36 drawn households in 2018, 2021 and 2024, the model's choice costs no more than a cent above the other choice, set as an input in a Simulation of its own. YAML cannot draw households or feed one simulation's result into another's inputs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QpcqEC8X8Zh3TKE8oN297V --- .../tests/test_ny_itemizes_property.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 policyengine_us/tests/test_ny_itemizes_property.py diff --git a/policyengine_us/tests/test_ny_itemizes_property.py b/policyengine_us/tests/test_ny_itemizes_property.py new file mode 100644 index 00000000000..a6625d4a47a --- /dev/null +++ b/policyengine_us/tests/test_ny_itemizes_property.py @@ -0,0 +1,113 @@ +"""New York's itemizing election never costs more State plus City tax. + +``ny_itemizes`` compares, in two branches, only the New York State and New +York City tax the election can move. This checks the choice against the +whole of ``ny_income_tax + nyc_income_tax``: for drawn households, the +model's choice leaves that tax no more than a cent above the other choice's, +calculated in a Simulation of its own with ``ny_itemizes`` set as an input. +YAML cannot draw households or set an input from another simulation's result. +""" + +import pytest + +pytest.importorskip("hypothesis") + +from hypothesis import ( # noqa: E402 + HealthCheck, + assume, + given, + settings, + strategies as st, +) + +from policyengine_us import Simulation # noqa: E402 + +YEARS = (2018, 2021, 2024) +TOLERANCE = 0.01 + + +@st.composite +def household(draw): + return { + "year": draw(st.sampled_from(YEARS)), + "nyc": draw(st.booleans()), + "joint": draw(st.booleans()), + "wages": draw(st.integers(20_000, 400_000)), + "tuition": draw(st.sampled_from([0, 2_000, 5_000, 10_000])), + "property_tax": draw(st.integers(0, 20_000)), + "child": draw(st.booleans()), + } + + +def build_situation(households, year, itemizes=None): + people, tax_units, marital_units, households_ = {}, {}, {}, {} + for i, case in enumerate(households): + head = f"head_{i}" + members = [head] + people[head] = { + "age": {year: 40}, + "employment_income": {year: case["wages"]}, + "qualified_tuition_expenses": {year: case["tuition"]}, + "real_estate_taxes": {year: case["property_tax"]}, + } + marital_units[f"marital_unit_{i}"] = {"members": [head]} + if case["joint"]: + spouse = f"spouse_{i}" + people[spouse] = {"age": {year: 40}} + members.append(spouse) + marital_units[f"marital_unit_{i}"]["members"].append(spouse) + if case["child"]: + child = f"child_{i}" + people[child] = {"age": {year: 5}} + members.append(child) + marital_units[f"child_marital_unit_{i}"] = {"members": [child]} + tax_units[f"tax_unit_{i}"] = {"members": members} + if itemizes is not None: + tax_units[f"tax_unit_{i}"]["ny_itemizes"] = {year: bool(itemizes[i])} + households_[f"household_{i}"] = { + "members": members, + "state_code": {year: "NY"}, + "in_nyc": {year: case["nyc"]}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "households": households_, + } + + +def state_and_city_tax(simulation, year): + return simulation.calculate("ny_income_tax", year) + simulation.calculate( + "nyc_income_tax", year + ) + + +# Each year builds two Simulations, whose time hardly depends on the number +# of households, so one example draws them all. +@settings( + max_examples=1, + deadline=None, + derandomize=True, + database=None, + suppress_health_check=[ + HealthCheck.too_slow, + HealthCheck.data_too_large, + HealthCheck.filter_too_much, + ], +) +@given(households=st.lists(household(), min_size=36, max_size=36)) +def test_ny_itemizing_election_costs_no_more_state_and_city_tax(households): + # Hypothesis first tries the simplest draw, 36 copies of one household. + assume(len({tuple(case.items()) for case in households}) > 1) + for year in YEARS: + cases = [case for case in households if case["year"] == year] + if not cases: + continue + model = Simulation(situation=build_situation(cases, year)) + itemizes = model.calculate("ny_itemizes", year) + tax = state_and_city_tax(model, year) + other = Simulation(situation=build_situation(cases, year, ~itemizes)) + other_tax = state_and_city_tax(other, year) + for case, chosen, mine, theirs in zip(cases, itemizes, tax, other_tax): + assert mine <= theirs + TOLERANCE, (case, bool(chosen), mine, theirs) From d10ade585510d8332b4bcbfd00304f512c6c8d80 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 16:45:17 +0000 Subject: [PATCH 08/10] Review ny_itemizes as a reader of the applied NY household credit ny_itemizes compares the applied household credit across its two branches: ny_eitc subtracts the applied credit, so the New York EITC can differ between the choices only where the applied credit does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QpcqEC8X8Zh3TKE8oN297V --- .../test_non_refundable_credit_downstream_consumers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py index 7f05a4bf64d..ceff448773b 100644 --- a/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py +++ b/policyengine_us/tests/code_health/test_non_refundable_credit_downstream_consumers.py @@ -38,6 +38,10 @@ "ny_household_credit": { "reforms/states/ny/wftc/ny_working_families_tax_credit.py", "variables/gov/states/ny/tax/income/credits/ny_eitc.py", + # ny_itemizes compares the applied credit across its two branches: + # ny_eitc subtracts the applied credit, so the New York EITC can + # differ between the choices only where the applied credit does. + "variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py", }, "oh_eitc": { "reforms/states/oh/eitc/oh_refundable_eitc_reform.py", From 2794e4f3d3e8f17002e7256a165bd233a1d3cf95 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 01:36:33 +0000 Subject: [PATCH 09/10] Skip shrinking in the NY election property test and fix stale comments A failing draw shrank for about 7 minutes and 10 GB, because each step rebuilds the Simulations; reporting the draw unshrunk keeps a failure inside the CI runner's memory. The YAML header and ny_deductions comment said every filer itemizes only for less tax, but a filer without tuition takes the larger deduction. Co-Authored-By: Claude Opus 5.5 --- .../taxable_income/deductions/itemized/ny_itemizes.yaml | 6 +++--- policyengine_us/tests/test_ny_itemizes_property.py | 4 ++++ .../states/ny/tax/income/taxable_income/ny_deductions.py | 8 +++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml index 7b2e83e2e6f..68ad237b03e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml @@ -1,6 +1,6 @@ -# New York's itemizing election is chosen by tax: among filers who may -# itemize, itemize only when that gives strictly less New York State plus -# New York City income tax. +# Among filers who may itemize for New York, a filer with college tuition +# expenses itemizes only when that lowers New York State plus New York City +# income tax; a filer without them takes the larger deduction. # # Hand values. 2021 single brackets: 4% to $8,500 ($340), 4.5% to $11,700 # ($144), 5.25% to $13,900 ($115.50), 5.9% to $21,400 ($442.50), then 5.97%; diff --git a/policyengine_us/tests/test_ny_itemizes_property.py b/policyengine_us/tests/test_ny_itemizes_property.py index a6625d4a47a..21b5e15cb20 100644 --- a/policyengine_us/tests/test_ny_itemizes_property.py +++ b/policyengine_us/tests/test_ny_itemizes_property.py @@ -14,6 +14,7 @@ from hypothesis import ( # noqa: E402 HealthCheck, + Phase, assume, given, settings, @@ -90,6 +91,9 @@ def state_and_city_tax(simulation, year): deadline=None, derandomize=True, database=None, + # Hypothesis reports a failing draw unshrunk, since shrinking would + # rebuild the Simulations at every step. + phases=[Phase.generate], suppress_health_check=[ HealthCheck.too_slow, HealthCheck.data_too_large, diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py index 2f1df460bf7..1fb96397a75 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/ny_deductions.py @@ -14,9 +14,11 @@ class ny_deductions(Variable): defined_for = StateCode.NY def formula(tax_unit, period, parameters): - # ny_itemizes makes the election: it itemizes when that gives less - # New York State plus City tax, and before 2018 only for a federal - # itemizer (N.Y. Tax Law 615(a)). + # ny_itemizes makes the election. Before 2018 only a federal itemizer + # may itemize (N.Y. Tax Law 615(a)). A filer with college tuition + # expenses itemizes when that lowers New York State plus City tax, and + # any other filer when the itemized deduction exceeds the standard + # deduction. return where( tax_unit("ny_itemizes", period), tax_unit("ny_itemized_deductions", period), From 9578a8ef8ecbe547d1a4b055192ea36ad0cd5662 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 5 Nov 2026 16:48:46 -0500 Subject: [PATCH 10/10] Pin the pre-2018 federal condition and cite the 2018 session law Before 2018 the Legislature allowed the New York itemized deduction only to a filer who itemized federally (Tax Law 615(a)). Case 16 is a 2017 separate filer in New York City whose federal choice turns on the New York election. If New York itemized, the federal deduction for state and local taxes would read $6,226.35, below the $6,350 standard deduction, so the return wouldn't itemize federally and the filer can't itemize for New York. Reading the federal choice from the branch where New York doesn't itemize fails this case and no other. The parameter and ny_itemizes now cite L.2018 ch. 59, Part JJ, sections 3 and 7, where the Legislature amends 615(a) for taxable years from 2018. ny_itemizes also cites 615(d)(4), where the Legislature allows no college tuition itemized deduction to a filer who claims the credit. Co-Authored-By: Claude Opus 5.5 --- .../federal_itemization_required.yaml | 2 ++ .../deductions/itemized/ny_itemizes.yaml | 36 +++++++++++++++++++ .../deductions/itemized/ny_itemizes.py | 7 +++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml index 841184a6fdb..58bc2b0eca6 100644 --- a/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml +++ b/policyengine_us/parameters/gov/states/ny/tax/income/deductions/itemized/federal_itemization_required.yaml @@ -7,6 +7,8 @@ metadata: period: year label: New York itemized deduction requires federal itemization reference: + - title: L.2018 ch. 59, Part JJ, sections 3 and 7 (S.7509-C), which amend section 615(a) for taxable years from 2018 (supports both values) + href: https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42 - title: N.Y. Tax Law 615(a), current text (supports the 2018 value) href: https://legislation.nysenate.gov/pdf/laws/TAX615 - title: 2017 Form IT-201 instructions, line 34 (supports the 2017 value) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml index 68ad237b03e..79493d9ec6a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.yaml @@ -462,3 +462,39 @@ # so the larger deduction is taken although the tax ties. ny_itemizes: [false, true] ny_deductions: [8_000, 10_000] + +# Case 16 pins the 2017 federal condition where the election itself moves the +# federal choice. A separate filer takes no federal tuition deduction. +- name: Case 16, a 2017 separate filer keeps the New York standard deduction when New York itemizing would leave its federal deductions below the federal standard deduction. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 22_000 + real_estate_taxes: 5_500 + qualified_tuition_expenses: 10_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SEPARATE + households: + household: + members: [person1] + state_code: NY + in_nyc: true + output: + # If New York itemized (15,500 > 8,000), its taxable income would fall to + # 6,500 and its tax to 4% x 6,500 = 260, and the federal deduction for + # state and local taxes would read 6,226.35, below the 6,350 standard + # deduction for a separate filer. That return doesn't itemize federally, + # so under 615(a) as of 2017 this filer can't elect New York itemizing. + # New York: taxable income 22,000 - 8,000 = 14,000; 340 + 144 + 115.50 + + # 5.9% x 100 = 605.40, less the 4% x 10,000 = 400 tuition credit: 205.40. + # City: 3.078% x 12,000 + 3.762% x 2,000 - 5 household credit = 439.60, + # less 63 + 0.171% x 12,000 + 0.228% x 2,000 = 88.08: 351.52. + ny_itemizes: false + ny_deductions: 8_000 + ny_income_tax: 205.40 + nyc_income_tax: 351.52 diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py index 3d18e6c7f2a..d82d90760ec 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemizes.py @@ -70,8 +70,13 @@ class ny_itemizes(Variable): defined_for = StateCode.NY reference = ( "https://www.nysenate.gov/legislation/laws/TAX/613", - # N.Y. Tax Law 615(a), current text (supports the 2018 rule). + # N.Y. Tax Law 615(a), current text (supports the 2018 rule), and + # 615(d)(4), where the Legislature allows no college tuition itemized + # deduction to a filer who claims the college tuition credit. "https://legislation.nysenate.gov/pdf/laws/TAX615", + # L.2018 ch. 59, Part JJ, sections 3 and 7: the Legislature amends + # 615(a) for taxable years from 2018 (supports both rules). + "https://legislation.nysenate.gov/pdf/bills/2017/S7509C#page=42", # 2017 Form IT-201 instructions, line 34 (supports the 2017 rule). "https://www.tax.ny.gov/pdf/2017/inc/it201i_2017.pdf#page=20", # 2018 Form IT-196 instructions, New for 2018.