From 8257601ba03f02749bf5da92648bebb665032038 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 22:21:07 -0400 Subject: [PATCH 1/6] Follow the statute on the ACTC housing-deduction bar and the joint filing test Section 24(d)(3) bars the refundable child tax credit only for a taxpayer who "elects to exclude any amount from gross income under section 911"; the section 911(c)(4)(A) housing deduction is a deduction, not an exclusion. The Schedule 8812 instructions bar every Form 2555 filer. For 2018-2025, section 6012(f)(2)(D) tests a claimable spouse on a joint return against the section 63(c)(5)(A) amount alone; Chart B adds the additional standard deduction for age or blindness. Under the statute-first rule (statute governs where it conflicts with a form or instructions), the model now follows the statute in both. Co-Authored-By: Claude Opus 5.5 --- .../statute-first-federal-income-tax.fixed.md | 1 + ...filing_requirement_dependent_invariants.py | 55 +++++---- .../ctc_credit_limit_worksheet_b_applies.yaml | 10 +- .../ctc/refundable/refundable_ctc.yaml | 17 +++ ...e_ctc_barred_by_section_911_exclusion.yaml | 42 +++++++ ...x_unit_is_required_to_file_dependents.yaml | 113 ++++++++++++++++-- policyengine_us/tools/section_911.py | 52 ++++++-- ...ble_ctc_barred_by_section_911_exclusion.py | 14 ++- .../gov/irs/tax_unit_is_required_to_file.py | 24 +++- 9 files changed, 275 insertions(+), 53 deletions(-) create mode 100644 changelog.d/statute-first-federal-income-tax.fixed.md diff --git a/changelog.d/statute-first-federal-income-tax.fixed.md b/changelog.d/statute-first-federal-income-tax.fixed.md new file mode 100644 index 00000000000..722ee77d1d4 --- /dev/null +++ b/changelog.d/statute-first-federal-income-tax.fixed.md @@ -0,0 +1 @@ +Follow the statute where IRS forms differ: the refundable child tax credit bar applies only to a section 911 exclusion (not the housing deduction alone), and a claimable spouse aged 65 or blind on a 2018-2025 joint return must file with income other than earned income over the section 63(c)(5)(A) amount. diff --git a/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py b/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py index 9b895cdd293..3fd639bdc12 100644 --- a/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py +++ b/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py @@ -9,8 +9,11 @@ unearned income test. YAML cases pin the published amounts one case at a time; these tests check what must hold for every input, which YAML cannot state. -Where the statute and the IRS charts differ (decision d1395: the age and -blindness addition in the 2018-2025 joint test, a dependent filing +On a 2018-2025 joint return, section 6012(f)(2)(D) tests a claimable +spouse's income other than earned income against the section 63(c)(5)(A) +amount alone, while Table 2 adds the additional standard deduction for age or +blindness; the model follows the statute, and property 10 states it. Where +the statute and the IRS charts differ otherwise (a dependent filing separately, and in 2026 blindness and the exact dollar), the tests below assert nothing that depends on the difference. @@ -29,10 +32,11 @@ 3. For a filer nobody can claim, the result depends on gross income alone: it is the same however that income is split between wages and interest. 4. On a 2025 joint return with no other income, where the spouse who can be - claimed is under 65 and not blind, the result is the unearned income test - (over $1,350), whatever the other spouse's age and blindness. (For a - claimed spouse who is 65 or older or blind, section 6012(f)(2)(D) and - Table 2 differ; that is d1395.) + claimed has only interest, the result is section 6012(f)(2)(D)'s unearned + income test (over $1,350), whatever either spouse's age and blindness. + Table 2 would add $1,600 for each of the claimed spouse's age 65 and + blindness; the grid's amounts between $1,350 and those sums tell the two + apart. `test_filing_requirement_properties` draws batches of tax units with Hypothesis (single, head of household, surviving spouse, separate and joint; @@ -54,11 +58,11 @@ income belongs on the dependent's own return. 9. Table 2 and the Medicaid implementation, as in 1 and 2, on the drawn single filers who can be claimed and have no self-employment income. -10. Lower bound (2025): on a single, separate or joint return, a head or - spouse who can be claimed and whose interest and dividends are over - Table 2's unearned income amount for them always makes the return - required. Table 2's amount is never below the statute's, so this holds - under either reading of d1395. +10. Lower bound (2025): on a single or separate return, a head or spouse + who can be claimed and whose interest and dividends are over Table 2's + unearned income amount for them always makes the return required. On a + joint return the amount is section 6012(f)(2)(D)'s: the section + 63(c)(5)(A) amount, $1,350, with no addition for age or blindness. `test_earned_income_sources_are_gross_income_sources`: @@ -215,16 +219,18 @@ def head_medicaid_requirement(simulation, units, year): def unearned_test_met(units): - """Whether Table 2's unearned income test is met by a head or spouse who - can be claimed, for single, separate and joint returns (2025 amounts).""" + """Whether the unearned income test is met by a head or spouse who can be + claimed (2025 amounts): Table 2's for single and separate returns, and + section 6012(f)(2)(D)'s, with no addition for age or blindness, for joint + returns.""" met = [] for unit in units: - married = unit["filing_status"] in ("SEPARATE", "JOINT") - per_condition = ( - TABLE_2_MARRIED_AGED_OR_BLIND_ADDITION - if married - else TABLE_2_AGED_OR_BLIND_ADDITION - ) + if unit["filing_status"] == "JOINT": + per_condition = 0 + elif unit["filing_status"] == "SEPARATE": + per_condition = TABLE_2_MARRIED_AGED_OR_BLIND_ADDITION + else: + per_condition = TABLE_2_AGED_OR_BLIND_ADDITION met.append( unit["filing_status"] in ("SINGLE", "SEPARATE", "JOINT") and any( @@ -292,8 +298,9 @@ def test_table_2_boundary_grid(): single_filer(age, blind, False, wages, gross - wages) for age, blind, gross, wages in splits ] - # Joint returns whose younger spouse, under 65 and not blind, can be - # claimed and has only interest. + # Joint returns whose spouse can be claimed and has only interest, with + # and without age 65 and blindness: section 6012(f)(2)(D) tests every + # such spouse at the section 63(c)(5)(A) amount alone. joint = [ { "filing_status": "JOINT", @@ -305,8 +312,8 @@ def test_table_2_boundary_grid(): "taxable_interest_income": 0, }, { - "age": 40, - "is_blind": False, + "age": spouse_age, + "is_blind": spouse_blind, "claimed_as_dependent_on_another_return": True, "taxable_interest_income": interest, }, @@ -315,6 +322,8 @@ def test_table_2_boundary_grid(): } for head_age in (45, 70, 80) for head_blind in (False, True) + for spouse_age in (40, 65) + for spouse_blind in (False, True) for interest in UNEARNED_GRID ] units = claimable + unclaimable + joint diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml index 44f9e6f5b7f..b552338a9bc 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml @@ -30,7 +30,12 @@ foreign_earned_income_exclusion: 0 ctc_credit_limit_worksheet_b_applies: false -- name: Housing deduction filer cannot use Worksheet B when worksheet deductions offset the stacking amount. +- name: A filer claiming only the housing deduction is not barred, so Worksheet B applies. + # Section 24(d)(3) bars a taxpayer who "elects to exclude any amount from + # gross income under section 911"; the section 911(c)(4)(A) housing + # deduction is a deduction, not an exclusion. The Schedule 8812 + # instructions would bar every Form 2555 filer; the model follows the + # statute, and Worksheet B reads the statutory bar. period: 2025 input: ctc_qualifying_children: 2 @@ -38,7 +43,8 @@ foreign_earned_income_exclusion_disallowed_deductions: 500 output: foreign_earned_income_exclusion: 0 - ctc_credit_limit_worksheet_b_applies: false + refundable_ctc_barred_by_section_911_exclusion: false + ctc_credit_limit_worksheet_b_applies: true - name: Does not apply in 2021, when the CTC was fully refundable. period: 2021 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml index 4b6ca135f18..3bd44a8c464 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml @@ -206,3 +206,20 @@ section_911_excluded_income: 0 # Without the section 911 bar, the same phase-in gives a $3,400 refund. refundable_ctc: 0 + +- name: A filer claiming only the foreign housing deduction gets the refundable credit + # Section 24(d)(3) bars only an election to exclude an amount under section + # 911; the section 911(c)(4)(A) housing deduction is not an exclusion. + # Phase-in: 15% x ($12,500 - $2,500) = $1,500, below the $1,700 refundable + # maximum and the $2,200 credit, with no tax to absorb the credit. + period: 2025 + input: + foreign_earned_income_exclusion_amount: 0 + foreign_housing_exclusion: 0 + foreign_housing_deduction: 1_000 + ctc: 2_200 + ctc_refundable_maximum: 1_700 + ctc_phase_in: 1_500 + ctc_limiting_tax_liability: 0 + output: + refundable_ctc: 1_500 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml index 4a1f10bf6fa..8f172b3ac4d 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml @@ -92,3 +92,45 @@ output: foreign_earned_income_exclusion: 0 refundable_ctc_barred_by_section_911_exclusion: true + +# Section 911(c)(4)(A) treats the housing amount of a filer without +# employer-provided housing as "a deduction allowable in computing adjusted +# gross income". A filer who claims only that deduction (Form 2555 line 50) +# excludes nothing, so section 24(d)(3) does not bar the refundable credit, +# although the Schedule 8812 instructions bar every Form 2555 filer. The model +# follows the statute. +- name: A housing deduction alone does not bar the refundable credit + period: 2025 + input: + foreign_earned_income_exclusion_amount: 0 + foreign_housing_exclusion: 0 + foreign_housing_deduction: 1_000 + output: + refundable_ctc_barred_by_section_911_exclusion: false + +- name: A housing deduction alone did not bar the credit in 2015 either + period: 2015 + input: + foreign_earned_income_exclusion_amount: 0 + foreign_housing_exclusion: 0 + foreign_housing_deduction: 1_000 + output: + refundable_ctc_barred_by_section_911_exclusion: false + +- name: A housing exclusion with a housing deduction is still barred + period: 2025 + input: + foreign_earned_income_exclusion_amount: 0 + foreign_housing_exclusion: 2_000 + foreign_housing_deduction: 1_000 + output: + refundable_ctc_barred_by_section_911_exclusion: true + +# The legacy aggregates include the housing deduction, so without Form 2555 +# leaves a positive aggregate is treated as an exclusion. +- name: A positive legacy aggregate is still treated as an exclusion + period: 2025 + input: + foreign_earned_income_exclusion: 1_000 + output: + refundable_ctc_barred_by_section_911_exclusion: true diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml index 17aff4458bf..1d946d8df50 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml @@ -5,10 +5,13 @@ # Chart B of the Form 1040 instructions (Tables 1 and 2 of Publication 501). # Section 6012(f) governs 2018 to 2025 and section 6012(a)(1) governs 2026. # For an unmarried filer who can be claimed, section 6012(f)(1) gives Chart -# B's result. Where the statute and the charts differ (decision d1395: the -# age and blindness addition in the 2018-2025 joint test, a dependent filing -# separately, and in 2026 blindness and the exact dollar), the cases stay -# clear of the difference. +# B's result. On a 2018-2025 joint return the statute and Chart B differ on +# the unearned income test of a claimable spouse aged 65 or blind: section +# 6012(f)(2)(D) names the section 63(c)(5)(A) amount alone, Chart B adds the +# additional standard deduction, and the model follows the statute (cases +# below). Where they differ otherwise (a dependent filing separately, and in +# 2026 blindness and the exact dollar), the cases stay clear of the +# difference. # 2025 amounts (Chart B, page 10 of # https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf): # Single dependents, under 65 and not blind, must file if @@ -19,8 +22,9 @@ # 65 or older or blind: $3,350, $17,750, and the larger of $3,350 or earned # income (up to $15,300) plus $2,450. 65 or older and blind: $5,350, # $19,750, and the larger of $5,350 or earned income plus $4,450. -# Married dependents, 65 or older or blind: unearned income over $2,950 -# (section 6012(f)(2)(D): over $1,350, with no addition). +# Married dependents, 65 or older or blind: Chart B says unearned income +# over $2,950; section 6012(f)(2)(D), which the model follows, says over +# $1,350, with no addition. # Filers nobody can claim: gross income only. The cases stay a dollar away # from the threshold itself, where Chart A ("at least") and section 6012(f)(1) @@ -402,7 +406,8 @@ - name: 2025, joint return whose spouse aged 65 can be claimed, unearned income over $2,950 # Required under both readings: over $1,350 + $1,600 (Chart B) and over - # $1,350 (section 6012(f)(2)(D)). At $2,950 or less they differ (d1395). + # $1,350 (section 6012(f)(2)(D)). At $2,950 or less they differ; the next + # case follows the statute. period: 2025 input: people: @@ -422,6 +427,100 @@ output: tax_unit_is_required_to_file: true +- name: 2025, joint return whose claimable spouse is aged 65 must file with $2,000 of interest + # Section 6012(f)(2)(D) excuses a joint return only if neither spouse is an + # individual described in section 63(c)(5) "who has income (other than + # earned income) in excess of the amount in effect under section + # 63(c)(5)(A)": $1,350 for 2025, with no addition for age or blindness. + # $2,000 is over $1,350, so a return is required. Chart B would add $1,600 + # (over $2,950) and excuse it. Gross income of $2,000 is under the joint + # standard deduction, so only this test makes the return required. + period: 2025 + input: + people: + head: + age: 40 + spouse: + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 2_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + tax_unit_is_required_to_file: true + +- name: 2025, a blind claimable spouse is tested at $1,350 too + period: 2025 + input: + people: + head: + age: 40 + spouse: + age: 30 + is_blind: true + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_351 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + # Over $1,350 by a dollar; Chart B would require over $2,950. + tax_unit_is_required_to_file: true + +- name: 2025, a claimable spouse aged 65 with $1,350 of interest need not file + period: 2025 + input: + people: + head: + age: 40 + spouse: + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_350 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + # "In excess of" $1,350: equal is not over. + tax_unit_is_required_to_file: false + +- name: 2026, section 6012(a)(1)(C)(i)(I) adds the additional standard deduction + # From 2026 the test is income other than earned income "in excess of the + # sum of the amount in effect under section 63(c)(5)(A) plus the additional + # standard deduction (if any) to which the individual is entitled", so the + # same $2,000 of interest does not require a return for the aged spouse. + period: 2026 + input: + people: + head: + age: 40 + spouse: + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 2_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + tax_unit_is_required_to_file: false + - name: 2025, a claimed spouse's farm profit is earned income, not unearned income # Farm income (Schedule 1 line 6) is earned income on the IRS dependent # worksheet, so the unearned income test does not apply to it. Gross income diff --git a/policyengine_us/tools/section_911.py b/policyengine_us/tools/section_911.py index 9a6b1f26f9a..25f6a7bd16f 100644 --- a/policyengine_us/tools/section_911.py +++ b/policyengine_us/tools/section_911.py @@ -67,17 +67,11 @@ def elects_section_911_exclusion(tax_unit, period): Form 2555 filers from Worksheet B, refundable CTC, and EITC. A filer who claims only the section 911(c)(4) housing deduction (Form - 2555 line 50) is also barred. That case rests on the IRS instructions, - not on the statute. Section 24(d)(3) applies to a taxpayer who "elects - to exclude any amount from gross income under section 911", and section - 911(c)(4)(A) treats the housing amount as "a deduction allowable in - computing adjusted gross income", not an exclusion. The Schedule 8812 - instructions bar every Form 2555 filer: Part II-A says "If you file Form - 2555, you cannot claim the additional child tax credit", and Credit - Limit Worksheet B applies only if "You are not filing Form 2555". - Publication 596 Rule 5 denies the EITC to anyone who files Form 2555 "to - deduct or exclude a foreign housing amount"; section 32(c)(1)(C) excludes - an individual who "claims the benefits of section 911". + 2555 line 50) also claims the benefits of section 911, so section + 32(c)(1)(C) ("claims the benefits of section 911") bars the EITC, as + Publication 596 Rule 5 says ("to deduct or exclude a foreign housing + amount"). The refundable child tax credit uses the narrower test in + `excludes_amount_under_section_911`. References: https://www.law.cornell.edu/uscode/text/26/911#a @@ -99,6 +93,42 @@ def elects_section_911_exclusion(tax_unit, period): ) +def excludes_amount_under_section_911(tax_unit, period): + """Whether the filer elects to exclude an amount under section 911(a). + + Section 24(d)(3) denies the refundable child tax credit to a taxpayer who + "elects to exclude any amount from gross income under section 911". + Section 911(c)(4)(A) treats the housing amount of a filer without + employer-provided housing as "a deduction allowable in computing adjusted + gross income", not an exclusion, so claiming only that deduction (Form + 2555 line 50) does not trigger the bar. The Schedule 8812 instructions + bar every Form 2555 filer ("If you file Form 2555, you cannot claim the + additional child tax credit"); the model follows the statute. + + With Form 2555 leaf inputs, the exclusions are the line 42 foreign earned + income exclusion and the line 36 housing exclusion, tested before the + line 44 allocable deductions so that deductions offsetting the stacking + amount cannot hide an exclusion. The legacy aggregates + (foreign_earned_income_exclusion and section_911_excluded_income) include + the housing deduction, so they cannot tell a deduction-only filer apart; + a positive aggregate is treated as an exclusion. Supply the leaves to + model a filer who claims only the deduction. + + References: + https://www.law.cornell.edu/uscode/text/26/24#d_3 + https://www.law.cornell.edu/uscode/text/26/911#a + https://www.law.cornell.edu/uscode/text/26/911#c_4 + https://www.irs.gov/pub/irs-prior/f2555--2025.pdf#page=3 + """ + if has_section_911_leaf_inputs(tax_unit, period): + return (tax_unit("foreign_earned_income_exclusion_amount", period) > 0) | ( + tax_unit("foreign_housing_exclusion", period) > 0 + ) + return (tax_unit("foreign_earned_income_exclusion", period) > 0) | ( + tax_unit("section_911_excluded_income", period) > 0 + ) + + def validate_section_911_batch_inputs(situation, default_period): """Reject ambiguous JSON batches before Core fills omitted inputs with zeros. diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py index ea457d74439..07d10092118 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py @@ -1,5 +1,5 @@ from policyengine_us.model_api import * -from policyengine_us.tools.section_911 import elects_section_911_exclusion +from policyengine_us.tools.section_911 import excludes_amount_under_section_911 class refundable_ctc_barred_by_section_911_exclusion(Variable): @@ -10,20 +10,26 @@ class refundable_ctc_barred_by_section_911_exclusion(Variable): "Whether section 24(d)(3) denies the refundable Child Tax Credit " "because the filer elects to exclude foreign earned income or housing " "amounts from gross income under section 911 (Form 2555). These " - "filers also skip Schedule 8812 Credit Limit Worksheet B." + "filers also skip Schedule 8812 Credit Limit Worksheet B. Claiming " + "only the section 911(c)(4) housing deduction is not an exclusion and " + "does not bar the credit, although the Schedule 8812 instructions bar " + "every Form 2555 filer; the model follows the statute." ) definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/24#d_3", # 2025 Instructions for Schedule 8812, Part II-A. "https://www.irs.gov/pub/irs-prior/i1040s8--2025.pdf#page=3", + # Section 911(c)(4)(A): the housing deduction is a deduction, not an + # exclusion. + "https://www.law.cornell.edu/uscode/text/26/911#c_4", # 2021 Schedule 8812: Part I-B has no Form 2555 condition. "https://www.irs.gov/pub/irs-prior/f1040s8--2021.pdf#page=1", ) def formula(tax_unit, period, parameters): p = parameters(period).gov.irs.credits.ctc.refundable - claims_section_911 = elects_section_911_exclusion(tax_unit, period) + excludes_section_911 = excludes_amount_under_section_911(tax_unit, period) # Section 24(i)(1)(A) (2021) switched off all of subsection (d), # including this bar, for filers whose principal place of abode was in # the United States for more than half the year and for bona fide @@ -32,4 +38,4 @@ def formula(tax_unit, period, parameters): bar_in_force = ( p.foreign_earned_income_exclusion_bar_applies and not p.fully_refundable ) - return claims_section_911 & bar_in_force + return excludes_section_911 & bar_in_force diff --git a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py index 8fd7416bbb7..ca3c4b64f1b 100644 --- a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py +++ b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py @@ -123,10 +123,16 @@ def formula(tax_unit, period, parameters): # amount plus their own additional standard deduction for age or # blindness: section 6012(a)(1)(C)(i)(I) and Publication 501, Table 2. # On a joint return the test applies to each spouse who can be claimed. - # Section 6012(f)(2)(D), which governs joint returns for 2018 to 2025, - # names the section 63(c)(5)(A) amount alone; Table 2 adds the - # additional standard deduction, and this follows Table 2 (open as - # decision d1395). Nobody else has an unearned income test. + # For 2018 to 2025, section 6012(f)(2)(D) sets the joint return test + # at "the amount in effect under section 63(c)(5)(A)" alone, with no + # addition for age or blindness, so a claimable spouse aged 65 or + # blind must file with more than that amount of income other than + # earned income. Chart B adds the additional standard deduction + # ($2,950 rather than $1,350 for 2025); the model follows the statute. + # For an unmarried filer, section 6012(f)(1) has no separate unearned + # income test, and Table 2's test with the addition is already implied + # by the gross income test against the limited standard deduction. + # Nobody else has an unearned income test. # # Table 2's other two tests need no code here. The standard deduction # in the threshold above is already the limited one for these filers @@ -160,8 +166,14 @@ def formula(tax_unit, period, parameters): additional_standard_deduction = aged_or_blind_count * tax_unit.project( p_standard.aged_or_blind.amount[filing_status] ) - unearned_income_threshold = ( - p_standard.dependent.amount + additional_standard_deduction + joint = filing_status == filing_status.possible_values.JOINT + statutory_joint_test = ( + joint & p.income.filing_requirement.standard_deduction_rule.applies + ) + unearned_income_threshold = p_standard.dependent.amount + where( + tax_unit.project(statutory_joint_test), + 0, + additional_standard_deduction, ) unearned_income_over_threshold = tax_unit.any( can_be_claimed & (unearned_income > unearned_income_threshold) From 7df1e8c6efd15949c1148e09b698cebb15f12a2b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 22:28:25 -0400 Subject: [PATCH 2/6] Count estate and trust royalties in EITC investment income Section 32(i)(2)(C) counts rents and royalties not derived in the ordinary course of a trade or business, and section 662(b) keeps a trust's character for its beneficiaries. Publication 596 Worksheet 1 line 8 takes royalties only from Schedule E line 23b, leaving out royalties received through a Schedule K-1 (Form 1041) box 5. Add estate_royalty_income (a classification of estate_income, default zero) as its own basket, floored at zero, and extend the EITC investment income invariants with it. Co-Authored-By: Claude Opus 5.5 --- ...ndent_eitc_investment_income_invariants.py | 49 +++++++++-- .../eitc_investment_income_estate_trust.yaml | 87 +++++++++++++++++++ .../eitc_relevant_investment_income.py | 24 ++++- .../person/estate/estate_royalty_income.py | 33 +++++++ 4 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 policyengine_us/variables/household/income/person/estate/estate_royalty_income.py diff --git a/policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.py b/policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.py index 3899de9e039..b5767164c07 100644 --- a/policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.py +++ b/policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.py @@ -13,7 +13,11 @@ Worksheet 1 lines 11 and 12 name (Schedule E lines 40, 34a column (d) and 34b column (c)). `passive_estate_income`, the passive subset of `estate_income`, defaults to zero; the draws leave it unset, or set it to all, half or none of -`estate_income`, or to an unrelated signed amount. +`estate_income`, or to an unrelated signed amount. Royalties received through +an estate or trust and not derived in a trade or business +(`estate_royalty_income`, Schedule K-1 (Form 1041) box 5) are their own +basket under 32(i)(2)(C), floored at zero, which Worksheet 1 line 8 leaves out +and the model counts under the statute. Hypothesis draws batches of tax units (single, head of household with dependents, joint with and without dependents), and a seeded population of 200 @@ -32,7 +36,8 @@ interest and dividends, plus their Schedule D gain with capital gain distributions floored at zero (line 7), plus their net rental, farm rental, passive partnership and passive estate and trust income less the signed part - also in earned income, floored at zero (line 13). + also in earned income, floored at zero (line 13), plus the estate and trust + royalty basket floored at zero (section 32(i)(2)(C)). 3. Bounds: it is at least the filers' interest and dividends (drawn nonnegative), so never negative, and at least that plus their capital gains floored at zero, so no passive loss (an estate or trust loss included) @@ -81,6 +86,9 @@ ESTATE = "estate_income" PASSIVE_ESTATE = "passive_estate_income" OVERLAP = "eitc_passive_income_also_in_earned_income" +# Section 32(i)(2)(C) royalties received through an estate or trust: a basket +# of their own, floored at zero. +ROYALTY = "estate_royalty_income" SE_EARNINGS = "partnership_self_employment_net_earnings" # The filer's amounts, which a dependent's income must not change. FILER_OUTPUTS = [ @@ -129,6 +137,7 @@ def investment_amounts(draw, dependent=False): ) ), SE_EARNINGS: draw(signed), + ROYALTY: draw(signed), **draw(estate_amounts()), } @@ -240,6 +249,19 @@ def _seeded_units(n=200): ], } ) + # Royalties come from their own stream, so the draws above stay as they + # were before the royalty basket existed. The stream and the 30% rate + # leave every basket deciding eligibility for some units (the coverage + # assertions in test_seeded_population). + royalty_rng = np.random.default_rng(SEED + 5) + for unit in units: + for amounts in [unit["head"], unit["spouse"], *unit["dependents"]]: + if amounts is not None: + amounts[ROYALTY] = ( + float(round(royalty_rng.uniform(-12_000, 12_000))) + if royalty_rng.random() < 0.3 + else 0.0 + ) return units @@ -355,29 +377,36 @@ def filer_sum(names): ) capital = np.maximum(0, filer_sum(CAPITAL_INPUTS) + distributions) passive = np.maximum(0, filer_sum(PASSIVE_INPUTS) - filer_sum([OVERLAP])) - expected = portfolio + capital + passive + royalties = np.maximum(0, filer_sum([ROYALTY])) + expected = portfolio + capital + passive + royalties # Units whose eligibility turns on farm rent alone, or on the overlap input # alone, so the seeded population shows each term is exercised. eligible = expected <= run["limit"] without_farm_rent = ( portfolio + capital + + royalties + np.maximum( 0, filer_sum([n for n in PASSIVE_INPUTS if n != "farm_rent_income"]) - filer_sum([OVERLAP]), ) ) - without_overlap = portfolio + capital + np.maximum(0, filer_sum(PASSIVE_INPUTS)) + without_overlap = ( + portfolio + capital + royalties + np.maximum(0, filer_sum(PASSIVE_INPUTS)) + ) without_estate = ( portfolio + capital + + royalties + np.maximum( 0, filer_sum([n for n in PASSIVE_INPUTS if n != PASSIVE_ESTATE]) - filer_sum([OVERLAP]), ) ) + without_royalties = portfolio + capital + passive + royalty_decides = eligible != (without_royalties <= run["limit"]) farm_rent_decides = eligible != (without_farm_rent <= run["limit"]) overlap_decides = eligible != (without_overlap <= run["limit"]) estate_decides = eligible != (without_estate <= run["limit"]) @@ -422,11 +451,19 @@ def filer_sum(names): - _by_person(units, OVERLAP), ), ) + + np.maximum(0, _unit_sum(run, _by_person(units, ROYALTY))) ) np.testing.assert_allclose( investment_income[unchanged], all_member[unchanged], atol=TOLERANCE ) - return has_dependent, unchanged, farm_rent_decides, overlap_decides, estate_decides + return ( + has_dependent, + unchanged, + farm_rent_decides, + overlap_decides, + estate_decides, + royalty_decides, + ) # A batch's cost is mostly per-variable overhead, so each example is a large @@ -453,6 +490,7 @@ def test_seeded_population(year): farm_rent_decides, overlap_decides, estate_decides, + royalty_decides, ) = _check(_seeded_units(), year) # The population exercises both branches of each property. assert has_dependent.sum() > 50 @@ -460,3 +498,4 @@ def test_seeded_population(year): assert farm_rent_decides.sum() > 2 assert overlap_decides.sum() > 2 assert estate_decides.sum() > 2 + assert royalty_decides.sum() > 2 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_investment_income_estate_trust.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_investment_income_estate_trust.yaml index 8f667559a0c..8b7f63bfaa9 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_investment_income_estate_trust.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_investment_income_estate_trust.yaml @@ -358,3 +358,90 @@ adjusted_gross_income: 31_601 # 4,213 - 15.98% x (31,601 - 22,720) = 4,213 - 1,419.1838. eitc: 2_793.8162 + +# Royalties received through an estate or trust. Section 32(i)(2)(C) counts +# "gross income from rents or royalties not derived in the ordinary course of +# a trade or business" over the deductions and interest allocable to it, and +# section 662(b) keeps the trust's character for the beneficiary. A +# beneficiary reports these Schedule K-1 (Form 1041) box 5 royalties in +# Schedule E Part III column (f). Publication 596 (2025) Worksheet 1 line 8 +# takes royalties only from Schedule E line 23b, so it leaves them out; the +# model follows the statute. The 2025 limit is 11,950 (Rev. Proc. 2024-40, +# section 2.06(2)). + +- name: Trust royalties alone exceed the 2025 investment income limit + period: 2025 + absolute_error_margin: 0.01 + input: + age: 30 + estate_income: 12_000 + estate_royalty_income: 12_000 + output: + eitc_relevant_investment_income: 12_000 + eitc_investment_income_eligible: false + # Worksheet 1 would count none of it, so the test would pass. + +- name: Trust royalties at the 2025 limit pass + period: 2025 + absolute_error_margin: 0.01 + input: + age: 30 + taxable_interest_income: 950 + estate_income: 11_000 + estate_royalty_income: 11_000 + output: + eitc_relevant_investment_income: 11_950 + eitc_investment_income_eligible: true + +- name: A net royalty loss floors its basket at zero and never offsets interest + period: 2025 + absolute_error_margin: 0.01 + input: + age: 30 + taxable_interest_income: 11_950 + estate_income: -2_000 + estate_royalty_income: -2_000 + output: + eitc_relevant_investment_income: 11_950 + eitc_investment_income_eligible: true + +- name: Royalties and passive estate income are separate baskets + period: 2025 + absolute_error_margin: 0.01 + input: + age: 30 + estate_income: 1_000 + estate_royalty_income: 3_000 + passive_estate_income: -2_000 + output: + # max(0, 3,000) royalties + max(0, -2,000) passive: the passive loss + # cannot reduce the royalty basket. + eitc_relevant_investment_income: 3_000 + +- name: Trust royalties end the credit for an otherwise eligible couple with one child + period: 2025 + absolute_error_margin: 1 + input: + people: + head: + age: 35 + employment_income: 15_000 + estate_income: 12_000 + estate_royalty_income: 12_000 + spouse: + age: 35 + child: + age: 5 + tax_units: + tax_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: TX + output: + # AGI is below the 2025 joint phase-out start of 30,470, so with + # estate_royalty_income of zero the model gives the one-child maximum, + # 4,328. + eitc_relevant_investment_income: 12_000 + eitc: 0 diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py b/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py index 6399554581b..9f0f2860665 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py @@ -31,12 +31,17 @@ class eitc_relevant_investment_income(Variable): "defaults to zero; nonpassive estate and trust income (Schedule E " "Part III columns (e) and (f), such as Schedule K-1 (Form 1041) box 5 " "annuities and income in respect of a decedent) is not on lines 11 " - "and 12." + "and 12. Box 5 royalties not derived in the ordinary course of a trade " + "or business (estate_royalty_income) are their own basket under 26 USC " + "32(i)(2)(C), net of their allocable deductions and floored at zero. " + "Worksheet 1 line 8 takes royalties only from Schedule E line 23b, so " + "it leaves these out; the model follows the statute." ) unit = USD definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/32#i_2", + "https://www.law.cornell.edu/uscode/text/26/662#b", "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=7", "https://www.irs.gov/pub/irs-prior/f8814--2025.pdf#page=1", "https://www.irs.gov/pub/irs-prior/i8814--2025.pdf#page=4", @@ -114,4 +119,19 @@ def formula(tax_unit, period, parameters): ) - tax_unit_non_dep_add( tax_unit, period, ["eitc_passive_income_also_in_earned_income"] ) - return portfolio_income + max_(0, capital_gains) + max_(0, passive_income) + # Section 32(i)(2)(C): rents and royalties not derived in the ordinary + # course of a trade or business, net of their allocable deductions, in + # a basket of their own. A beneficiary's box 5 royalties keep that + # character (section 662(b)) and are nonpassive, so they are not in + # the passive basket. Worksheet 1 line 8 takes royalties only from + # Schedule E line 23b, leaving these out; the model follows the + # statute. + nonbusiness_royalties = tax_unit_non_dep_add( + tax_unit, period, ["estate_royalty_income"] + ) + return ( + portfolio_income + + max_(0, capital_gains) + + max_(0, nonbusiness_royalties) + + max_(0, passive_income) + ) diff --git a/policyengine_us/variables/household/income/person/estate/estate_royalty_income.py b/policyengine_us/variables/household/income/person/estate/estate_royalty_income.py new file mode 100644 index 00000000000..2f19a5341d9 --- /dev/null +++ b/policyengine_us/variables/household/income/person/estate/estate_royalty_income.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class estate_royalty_income(Variable): + value_type = float + entity = Person + label = "Estate and trust royalties" + unit = USD + documentation = ( + "Royalties included in estate_income that are not derived in the " + "ordinary course of a trade or business, net of the deductions and " + "interest clearly and directly allocable to them: Schedule K-1 (Form " + "1041) box 5 royalties, reported in Schedule E Part III column (f). " + "Section 662(b) gives a beneficiary's share of trust income the " + "character it had in the trust, so 26 USC 32(i)(2)(C) counts these " + "royalties as disqualified income for the EITC. Publication 596 " + "Worksheet 1 line 8 takes royalties only from Schedule E line 23b " + "(Part I) and so leaves them out; the model follows the statute. " + "Enter income as a positive amount and a net loss as a negative " + "amount. The default is zero. This classifies income already counted " + "in estate_income and does not represent additional income; box 5 " + "also holds annuities and income in respect of a decedent, which are " + "not royalties and are not included here." + ) + definition_period = YEAR + default_value = 0 + reference = ( + "https://www.law.cornell.edu/uscode/text/26/32#i_2_C", + "https://www.law.cornell.edu/uscode/text/26/662#b", + "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=7", + "https://www.irs.gov/pub/irs-prior/f1041sk1--2025.pdf#page=2", + "https://www.irs.gov/pub/irs-prior/i1041sk1--2025.pdf#page=2", + ) From 7b1af2d5b270cede5b88043c2b1e88dd6983238d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 22:38:59 -0400 Subject: [PATCH 3/6] Limit the section 129 exclusion by an absent spouse's earned income Section 129(b)(1)(B) limits the dependent care assistance exclusion of an employee married at the end of the year to the lesser of the employee's and the spouse's earned income. Section 129(a)(2)(C) imports the section 21(e)(4) living-apart rule only for the dollar cap; the Form 2441 line 19 instructions extend it to the earned income limit. Add absent_spouse_earned_income (defaulting to the existing limit, so results change only when it is supplied) and apply it to separate filers and separated heads of household. Extend the exclusion invariants with it. Co-Authored-By: Claude Opus 5.5 --- .../statute-first-federal-income-tax.fixed.md | 2 +- ...nt_care_assistance_exclusion_invariants.py | 60 ++++++++-- .../dependent_care_assistance_exclusion.yaml | 104 ++++++++++++++++++ .../cdcc/absent_spouse_earned_income.py | 28 +++++ .../dependent_care_assistance_exclusion.py | 32 +++++- 5 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py diff --git a/changelog.d/statute-first-federal-income-tax.fixed.md b/changelog.d/statute-first-federal-income-tax.fixed.md index 722ee77d1d4..1a5bb745b36 100644 --- a/changelog.d/statute-first-federal-income-tax.fixed.md +++ b/changelog.d/statute-first-federal-income-tax.fixed.md @@ -1 +1 @@ -Follow the statute where IRS forms differ: the refundable child tax credit bar applies only to a section 911 exclusion (not the housing deduction alone), and a claimable spouse aged 65 or blind on a 2018-2025 joint return must file with income other than earned income over the section 63(c)(5)(A) amount. +Follow the statute where IRS forms differ: the refundable child tax credit bar applies only to a section 911 exclusion (not the housing deduction alone); a claimable spouse aged 65 or blind on a 2018-2025 joint return must file with income other than earned income over the section 63(c)(5)(A) amount; estate and trust royalties count as EITC investment income; and the dependent care assistance exclusion of a married taxpayer who does not file jointly is limited by the absent spouse's earned income. diff --git a/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py b/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py index 024d1338a81..b4a8bf80c69 100644 --- a/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py +++ b/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py @@ -24,6 +24,10 @@ 1. Differential: dependent_care_assistance_exclusion equals an independent numpy min of the benefits, the cap printed on Form 2441 line 21 for the year (not the model's parameter) and the lesser of the filers' earnings. + A separate filer is married at the end of the year, so section + 129(b)(1)(B) also limits the exclusion to the absent spouse's earned + income (absent_spouse_earned_income, drawn for every separate filer), + including one treated as unmarried for the cap under section 21(e)(4). 2. Bound: 0 <= exclusion <= benefits, the year/status cap and earned income. 3. Monotone: raising benefits never lowers the exclusion. 4. California: ca_dependent_care_assistance_exclusion equals the same min with @@ -97,6 +101,11 @@ def tax_units(draw): "spouse": draw(filers()) if filing_status == "JOINT" else None, "children": draw(st.integers(0, 3)), "is_separated": draw(st.booleans()) if filing_status == "SEPARATE" else False, + "absent_spouse": ( + draw(st.one_of(wages, st.integers(0, 6_000).map(float))) + if filing_status == "SEPARATE" + else 0.0 + ), } @@ -129,6 +138,17 @@ def filer(): and bool(rng.integers(0, 2)), } ) + # The absent spouse's earnings come from their own stream, so the draws + # above stay as they were. + spouse_rng = np.random.default_rng(SEED + 1) + for unit in units: + # Half the draws are low, where the spouse's earnings can bind. + high = spouse_rng.random() < 0.5 + unit["absent_spouse"] = ( + float(spouse_rng.integers(0, 150_001 if high else 6_001)) + if unit["filing_status"] == "SEPARATE" + else 0.0 + ) return units @@ -165,6 +185,9 @@ def add_filer(name, values, role): groups["tax_units"][f"tax_unit_{i}"] = { "members": members, "filing_status": unit["filing_status"], + # Supplied for every unit, because a partly supplied input is zero + # for the others; only separate filers read it. + "absent_spouse_earned_income": unit["absent_spouse"], } groups["households"][f"household_{i}"] = { "members": members, @@ -176,7 +199,7 @@ def add_filer(name, values, role): } for kind in ("tax_units", "households"): for group in groups[kind].values(): - for key in ("filing_status", "state_code"): + for key in ("filing_status", "state_code", "absent_spouse_earned_income"): if key in group: group[key] = {year: group[key]} return {"people": people, **groups} @@ -234,10 +257,20 @@ def _expected(units, year, caps, *, raised=False): "filing_status" ] == "SEPARATE" and not _treated_as_unmarried(unit) cap.append(caps[1] if ordinary_separate else caps[0]) - earned.append(min(f["employment_income"] for f in filers)) + earned.append(_earned_income_limit(unit)) return np.minimum(np.array(benefits), np.minimum(cap, earned)) +def _earned_income_limit(unit): + """Section 129(b): the filers' lesser earnings, and for a separate filer + (married at the end of the year) also the absent spouse's earnings.""" + filers = [unit["head"]] + ([unit["spouse"]] if unit["spouse"] else []) + limit = min(f["employment_income"] for f in filers) + if unit["filing_status"] == "SEPARATE": + limit = min(limit, unit["absent_spouse"]) + return limit + + def _check(units, year): base, raised = _run(units, year) exclusion = base["dependent_care_assistance_exclusion"] @@ -273,14 +306,7 @@ def _check(units, year): for unit in units ] ) - earned_income_limits = np.array( - [ - min(unit["head"]["employment_income"], unit["spouse"]["employment_income"]) - if unit["spouse"] - else unit["head"]["employment_income"] - for unit in units - ] - ) + earned_income_limits = np.array([_earned_income_limit(unit) for unit in units]) assert (exclusion <= caps + TOLERANCE).all() assert (exclusion <= earned_income_limits + TOLERANCE).all() @@ -328,4 +354,16 @@ def test_dependent_care_assistance_exclusion_invariants(year, units): @pytest.mark.parametrize("year", YEARS) def test_seeded_population(year): - _check(_seeded_units(), year) + units = _seeded_units() + _check(units, year) + # Some separate filers have an absent spouse whose earnings bind, among + # them filers treated as unmarried for the cap. + binding = [ + unit + for unit in units + if unit["filing_status"] == "SEPARATE" + and unit["absent_spouse"] + < min(unit["head"]["employment_income"], unit["head"]["benefits"]) + ] + assert len(binding) > 5 + assert any(_treated_as_unmarried(unit) for unit in binding) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml index 5d08cd87b7b..e5ac357a4cb 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml @@ -404,3 +404,107 @@ cdcc_treated_as_unmarried: true # min($6,000 benefits, $5,000 unmarried cap, $38,000 earnings) = $5,000. dependent_care_assistance_exclusion: 5_000 + +# Section 129(b)(1)(B): for an employee "married at the close of such taxable +# year", the exclusion cannot exceed the lesser of the employee's and the +# spouse's earned income. Section 129(a)(2)(C) imports the section 21(e)(4) +# living-apart rule only "for purposes of this paragraph" (the dollar cap), +# so a separated filer treated as unmarried for the cap is still married for +# the earned income limit. The 2025 Form 2441 line 19 instructions let that +# filer enter their own earned income; the model follows the statute. +- name: A separated filer treated as unmarried is limited by the absent spouse's earnings + period: 2025 + input: + people: + person1: + age: 35 + employment_income: 38_000 + dependent_care_employer_benefits: 4_000 + is_separated: true + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: SEPARATE + absent_spouse_earned_income: 2_000 + output: + cdcc_treated_as_unmarried: true + # min($4,000 benefits, $5,000 unmarried cap, min($38,000, $2,000)). + dependent_care_assistance_exclusion: 2_000 + +- name: A separate filer not living apart is limited by the spouse's earnings too + period: 2025 + input: + people: + person1: + age: 35 + employment_income: 38_000 + dependent_care_employer_benefits: 2_500 + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: SEPARATE + absent_spouse_earned_income: 1_000 + output: + cdcc_treated_as_unmarried: false + # min($2,500 benefits, $2,500 MFS cap, min($38,000, $1,000)). + dependent_care_assistance_exclusion: 1_000 + +- name: A separated head of household is limited by the absent spouse's earnings + period: 2025 + input: + people: + person1: + age: 35 + employment_income: 38_000 + dependent_care_employer_benefits: 4_000 + is_separated: true + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: HEAD_OF_HOUSEHOLD + absent_spouse_earned_income: 3_000 + output: + dependent_care_assistance_exclusion: 3_000 + +- name: Without the absent spouse's earnings, the filer's own earnings apply + period: 2025 + input: + people: + person1: + age: 35 + employment_income: 38_000 + dependent_care_employer_benefits: 4_000 + is_separated: true + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: SEPARATE + output: + absent_spouse_earned_income: 38_000 + dependent_care_assistance_exclusion: 4_000 + +- name: An unmarried head of household is not limited by an absent spouse's earnings + period: 2025 + input: + people: + person1: + age: 35 + employment_income: 38_000 + dependent_care_employer_benefits: 4_000 + person2: + age: 5 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: HEAD_OF_HOUSEHOLD + absent_spouse_earned_income: 0 + output: + dependent_care_assistance_exclusion: 4_000 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py b/policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py new file mode 100644 index 00000000000..1f2ed2478c2 --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class absent_spouse_earned_income(Variable): + value_type = float + entity = TaxUnit + label = "Earned income of a married taxpayer's spouse who is not on the return" + documentation = ( + "For a taxpayer who is married at the end of the year but does not " + "file a joint return (a separate filer, or a separated filer who " + "files as head of household), the spouse's earned income for the " + "section 129(b)(1)(B) dependent care assistance limit, including any " + "amount the spouse is deemed to earn as a student or as incapable of " + "self-care under section 129(b)(2). The spouse is not a member of the " + "tax unit, so the model has no other record of these earnings. If it " + "is not supplied, it defaults to the earned income limit that applies " + "without it (min_head_spouse_earned, the taxpayer's own earnings on a " + "return that is not joint), which leaves the limit unchanged." + ) + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/129#b_1", + "https://www.law.cornell.edu/uscode/text/26/129#b_2", + ) + + def formula(tax_unit, period, parameters): + return tax_unit("min_head_spouse_earned", period) diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py b/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py index 3746d08563b..a976b10f18b 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py @@ -17,6 +17,8 @@ class dependent_care_assistance_exclusion(Variable): "https://www.law.cornell.edu/uscode/text/26/129#a_2", # Dollar cap. "https://www.law.cornell.edu/uscode/text/26/129#a_2_C", # Marital status. "https://www.law.cornell.edu/uscode/text/26/129#b", # Earned income limit. + # Form 2441 line 19 (spouse's earned income). + "https://www.irs.gov/pub/irs-prior/i2441--2025.pdf#page=5", # ARPA section 9632 temporarily increased the cap for 2021. "https://www.govinfo.gov/content/pkg/PLAW-117publ2/pdf/PLAW-117publ2.pdf#page=158", # Form 2441 Part III lines 12-26 compute the excluded benefit. @@ -43,9 +45,31 @@ def formula(tax_unit, period, parameters): p.reduction_amount[filing_status], ) # Section 129(b) earned-income limitation: the exclusion cannot exceed - # the taxpayer's earned income, or, for a married taxpayer, the lesser - # of the taxpayer's and spouse's earned income (Form 2441 lines 18-19). - # min_head_spouse_earned returns the taxpayer's earnings when unmarried - # and the lesser of the two when married filing jointly. + # the taxpayer's earned income, or, for a taxpayer married at the end + # of the year, the lesser of the taxpayer's and the spouse's earned + # income (Form 2441 lines 18-19). min_head_spouse_earned returns the + # taxpayer's earnings when unmarried and the lesser of the two when + # married filing jointly. earned_income_limit = tax_unit("min_head_spouse_earned", period) + # A married taxpayer who does not file jointly (a separate filer, or a + # separated filer filing as head of household) is still married for + # section 129(b)(1)(B). Section 129(a)(2)(C) imports the section + # 21(e)(4) rule for living apart "for purposes of this paragraph", + # the dollar cap, and section 129(b) does not import it. The Form 2441 + # line 19 instructions let a filer treated as unmarried under that + # rule enter their own earned income; the model follows the statute + # and uses the spouse's earned income, absent_spouse_earned_income. + separate = filing_status == filing_status.possible_values.SEPARATE + joint = filing_status == filing_status.possible_values.JOINT + person = tax_unit.members + separated = tax_unit.any( + person("is_tax_unit_head_or_spouse", period) + & person("is_separated", period) + ) + married_not_joint = separate | (separated & ~joint) + earned_income_limit = where( + married_not_joint, + min_(earned_income_limit, tax_unit("absent_spouse_earned_income", period)), + earned_income_limit, + ) return min_(benefits, min_(dollar_cap, earned_income_limit)) From 770c799d8a17e9361e9b3601485af11fec28a008 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:15:19 -0400 Subject: [PATCH 4/6] Apply former section 32(c)(1)(F) through 2020 Former 26 U.S.C. 32(c)(1)(F) denied the EITC to an eligible individual with qualifying children none of whom is taken into account because of the identification requirement of 32(c)(3)(D). The 2018-2020 Form 1040 instructions instead route such a filer to the credit without a qualifying child. ARPA section 9622 struck the subparagraph from 2021. Add a parameter (true through 2020) and the bar in eitc_eligible; 2020 example: $0 under the statute, $459 on the instruction route. Co-Authored-By: Claude Opus 5.5 --- ...ying_child_without_identification_bar.yaml | 17 ++++ .../credits/earned_income/eitc_eligible.yaml | 80 +++++++++++++++++++ .../eligibility/eitc_eligible.py | 16 ++++ 3 files changed, 113 insertions(+) create mode 100644 policyengine_us/parameters/gov/irs/credits/eitc/eligibility/qualifying_child_without_identification_bar.yaml diff --git a/policyengine_us/parameters/gov/irs/credits/eitc/eligibility/qualifying_child_without_identification_bar.yaml b/policyengine_us/parameters/gov/irs/credits/eitc/eligibility/qualifying_child_without_identification_bar.yaml new file mode 100644 index 00000000000..ef00ab4a7f7 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/credits/eitc/eligibility/qualifying_child_without_identification_bar.yaml @@ -0,0 +1,17 @@ +description: The Internal Revenue Service denies the earned income tax credit to a filer who has qualifying children none of whom meets the identification requirement, when this is true. +values: + # Former 26 U.S.C. 32(c)(1)(F), in the 2015 Code edition; earlier years + # are not separately verified. + 0000-01-01: true + # American Rescue Plan Act section 9622 struck subparagraph (F) for + # taxable years beginning after December 31, 2020. + 2021-01-01: false +metadata: + unit: bool + period: year + label: EITC bar on filers whose qualifying children all lack identification + reference: + - title: 26 U.S.C. 32(c)(1)(F) (2015 edition) + href: https://www.govinfo.gov/content/pkg/USCODE-2015-title26/html/USCODE-2015-title26-subtitleA-chap1-subchapA-partIV-subpartC-sec32.htm + - title: Public Law 117-2, section 9622 + href: https://www.congress.gov/117/plaws/publ2/PLAW-117publ2.pdf#page=151 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml index b8de8659af6..cf9734b981a 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml @@ -134,3 +134,83 @@ output: foreign_earned_income_exclusion: 0 eitc_eligible: false + +# Former 26 U.S.C. 32(c)(1)(F), through 2020: "No credit shall be allowed +# under this section to any eligible individual who has one or more qualifying +# children if no qualifying child of such individual is taken into account +# under subsection (b) by reason of paragraph (3)(D)." The 2018-2020 Form 1040 +# instructions send such a filer to the credit without a qualifying child; the +# model follows the statute. ARPA section 9622 struck the bar from 2021. +- name: 2020 filer whose only qualifying child lacks an SSN gets no EITC + period: 2020 + input: + people: + head: + age: 30 + employment_income: 6_000 + ssn_card_type: CITIZEN + child: + age: 5 + ssn_card_type: NONE + tax_units: + tax_unit: + members: [head, child] + tax_unit_is_filer: true + households: + household: + members: [head, child] + state_code: TX + output: + eitc_child_count: 0 + eitc_eligible: false + # The instructions' route would give the childless credit: + # min(7.65% x $6,000, $538) = $459. + eitc: 0 + +- name: 2021 the same filer can claim the credit without a qualifying child + period: 2021 + input: + people: + head: + age: 30 + employment_income: 6_000 + ssn_card_type: CITIZEN + child: + age: 5 + ssn_card_type: NONE + tax_units: + tax_unit: + members: [head, child] + tax_unit_is_filer: true + households: + household: + members: [head, child] + state_code: TX + output: + eitc_child_count: 0 + eitc_eligible: true + +- name: 2020 filer with one child with an SSN and one without keeps the credit + period: 2020 + input: + people: + head: + age: 30 + employment_income: 6_000 + ssn_card_type: CITIZEN + child1: + age: 5 + ssn_card_type: CITIZEN + child2: + age: 3 + ssn_card_type: NONE + tax_units: + tax_unit: + members: [head, child1, child2] + households: + household: + members: [head, child1, child2] + state_code: TX + output: + eitc_child_count: 1 + eitc_eligible: true diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py index f4be2784cbf..11891743c9b 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py @@ -11,6 +11,8 @@ class eitc_eligible(Variable): "https://www.law.cornell.edu/uscode/text/26/32#c_1_A", # No EITC for an individual who claims the benefits of section 911. "https://www.law.cornell.edu/uscode/text/26/32#c_1_C", + # Former section 32(c)(1)(F), struck from 2021. + "https://www.govinfo.gov/content/pkg/USCODE-2015-title26/html/USCODE-2015-title26-subtitleA-chap1-subchapA-partIV-subpartC-sec32.htm", ) def formula(tax_unit, period, parameters): @@ -31,6 +33,20 @@ def formula(tax_unit, period, parameters): & filers_has_ssn & ~claims_section_911 ) + # Former section 32(c)(1)(F), through 2020: "No credit shall be + # allowed under this section to any eligible individual who has one or + # more qualifying children if no qualifying child of such individual + # is taken into account under subsection (b) by reason of paragraph + # (3)(D)", the identification requirement. The 2018-2020 Form 1040 + # instructions instead send such a filer to the credit without a + # qualifying child; the model follows the statute. + if eitc.eligibility.qualifying_child_without_identification_bar: + person = tax_unit.members + has_qualifying_child = tax_unit.any( + person("is_eitc_qualifying_child", period) + ) + no_child_counted = tax_unit("eitc_child_count", period) == 0 + eligible = eligible & ~(has_qualifying_child & no_child_counted) # This parameter is true if separate filers are eligible. if eitc.eligibility.separate_filer: return eligible From 2883485b6e7dceb622e6490ffb2e462d2aa3147b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:15:19 -0400 Subject: [PATCH 5/6] Revert "Limit the section 129 exclusion by an absent spouse's earned income" This reverts commit 7b1af2d5b270cede5b88043c2b1e88dd6983238d. --- .../statute-first-federal-income-tax.fixed.md | 2 +- ...nt_care_assistance_exclusion_invariants.py | 60 ++-------- .../dependent_care_assistance_exclusion.yaml | 104 ------------------ .../cdcc/absent_spouse_earned_income.py | 28 ----- .../dependent_care_assistance_exclusion.py | 32 +----- 5 files changed, 16 insertions(+), 210 deletions(-) delete mode 100644 policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py diff --git a/changelog.d/statute-first-federal-income-tax.fixed.md b/changelog.d/statute-first-federal-income-tax.fixed.md index 1a5bb745b36..722ee77d1d4 100644 --- a/changelog.d/statute-first-federal-income-tax.fixed.md +++ b/changelog.d/statute-first-federal-income-tax.fixed.md @@ -1 +1 @@ -Follow the statute where IRS forms differ: the refundable child tax credit bar applies only to a section 911 exclusion (not the housing deduction alone); a claimable spouse aged 65 or blind on a 2018-2025 joint return must file with income other than earned income over the section 63(c)(5)(A) amount; estate and trust royalties count as EITC investment income; and the dependent care assistance exclusion of a married taxpayer who does not file jointly is limited by the absent spouse's earned income. +Follow the statute where IRS forms differ: the refundable child tax credit bar applies only to a section 911 exclusion (not the housing deduction alone), and a claimable spouse aged 65 or blind on a 2018-2025 joint return must file with income other than earned income over the section 63(c)(5)(A) amount. diff --git a/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py b/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py index b4a8bf80c69..024d1338a81 100644 --- a/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py +++ b/policyengine_us/tests/core/test_dependent_care_assistance_exclusion_invariants.py @@ -24,10 +24,6 @@ 1. Differential: dependent_care_assistance_exclusion equals an independent numpy min of the benefits, the cap printed on Form 2441 line 21 for the year (not the model's parameter) and the lesser of the filers' earnings. - A separate filer is married at the end of the year, so section - 129(b)(1)(B) also limits the exclusion to the absent spouse's earned - income (absent_spouse_earned_income, drawn for every separate filer), - including one treated as unmarried for the cap under section 21(e)(4). 2. Bound: 0 <= exclusion <= benefits, the year/status cap and earned income. 3. Monotone: raising benefits never lowers the exclusion. 4. California: ca_dependent_care_assistance_exclusion equals the same min with @@ -101,11 +97,6 @@ def tax_units(draw): "spouse": draw(filers()) if filing_status == "JOINT" else None, "children": draw(st.integers(0, 3)), "is_separated": draw(st.booleans()) if filing_status == "SEPARATE" else False, - "absent_spouse": ( - draw(st.one_of(wages, st.integers(0, 6_000).map(float))) - if filing_status == "SEPARATE" - else 0.0 - ), } @@ -138,17 +129,6 @@ def filer(): and bool(rng.integers(0, 2)), } ) - # The absent spouse's earnings come from their own stream, so the draws - # above stay as they were. - spouse_rng = np.random.default_rng(SEED + 1) - for unit in units: - # Half the draws are low, where the spouse's earnings can bind. - high = spouse_rng.random() < 0.5 - unit["absent_spouse"] = ( - float(spouse_rng.integers(0, 150_001 if high else 6_001)) - if unit["filing_status"] == "SEPARATE" - else 0.0 - ) return units @@ -185,9 +165,6 @@ def add_filer(name, values, role): groups["tax_units"][f"tax_unit_{i}"] = { "members": members, "filing_status": unit["filing_status"], - # Supplied for every unit, because a partly supplied input is zero - # for the others; only separate filers read it. - "absent_spouse_earned_income": unit["absent_spouse"], } groups["households"][f"household_{i}"] = { "members": members, @@ -199,7 +176,7 @@ def add_filer(name, values, role): } for kind in ("tax_units", "households"): for group in groups[kind].values(): - for key in ("filing_status", "state_code", "absent_spouse_earned_income"): + for key in ("filing_status", "state_code"): if key in group: group[key] = {year: group[key]} return {"people": people, **groups} @@ -257,20 +234,10 @@ def _expected(units, year, caps, *, raised=False): "filing_status" ] == "SEPARATE" and not _treated_as_unmarried(unit) cap.append(caps[1] if ordinary_separate else caps[0]) - earned.append(_earned_income_limit(unit)) + earned.append(min(f["employment_income"] for f in filers)) return np.minimum(np.array(benefits), np.minimum(cap, earned)) -def _earned_income_limit(unit): - """Section 129(b): the filers' lesser earnings, and for a separate filer - (married at the end of the year) also the absent spouse's earnings.""" - filers = [unit["head"]] + ([unit["spouse"]] if unit["spouse"] else []) - limit = min(f["employment_income"] for f in filers) - if unit["filing_status"] == "SEPARATE": - limit = min(limit, unit["absent_spouse"]) - return limit - - def _check(units, year): base, raised = _run(units, year) exclusion = base["dependent_care_assistance_exclusion"] @@ -306,7 +273,14 @@ def _check(units, year): for unit in units ] ) - earned_income_limits = np.array([_earned_income_limit(unit) for unit in units]) + earned_income_limits = np.array( + [ + min(unit["head"]["employment_income"], unit["spouse"]["employment_income"]) + if unit["spouse"] + else unit["head"]["employment_income"] + for unit in units + ] + ) assert (exclusion <= caps + TOLERANCE).all() assert (exclusion <= earned_income_limits + TOLERANCE).all() @@ -354,16 +328,4 @@ def test_dependent_care_assistance_exclusion_invariants(year, units): @pytest.mark.parametrize("year", YEARS) def test_seeded_population(year): - units = _seeded_units() - _check(units, year) - # Some separate filers have an absent spouse whose earnings bind, among - # them filers treated as unmarried for the cap. - binding = [ - unit - for unit in units - if unit["filing_status"] == "SEPARATE" - and unit["absent_spouse"] - < min(unit["head"]["employment_income"], unit["head"]["benefits"]) - ] - assert len(binding) > 5 - assert any(_treated_as_unmarried(unit) for unit in binding) + _check(_seeded_units(), year) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml index e5ac357a4cb..5d08cd87b7b 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.yaml @@ -404,107 +404,3 @@ cdcc_treated_as_unmarried: true # min($6,000 benefits, $5,000 unmarried cap, $38,000 earnings) = $5,000. dependent_care_assistance_exclusion: 5_000 - -# Section 129(b)(1)(B): for an employee "married at the close of such taxable -# year", the exclusion cannot exceed the lesser of the employee's and the -# spouse's earned income. Section 129(a)(2)(C) imports the section 21(e)(4) -# living-apart rule only "for purposes of this paragraph" (the dollar cap), -# so a separated filer treated as unmarried for the cap is still married for -# the earned income limit. The 2025 Form 2441 line 19 instructions let that -# filer enter their own earned income; the model follows the statute. -- name: A separated filer treated as unmarried is limited by the absent spouse's earnings - period: 2025 - input: - people: - person1: - age: 35 - employment_income: 38_000 - dependent_care_employer_benefits: 4_000 - is_separated: true - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - filing_status: SEPARATE - absent_spouse_earned_income: 2_000 - output: - cdcc_treated_as_unmarried: true - # min($4,000 benefits, $5,000 unmarried cap, min($38,000, $2,000)). - dependent_care_assistance_exclusion: 2_000 - -- name: A separate filer not living apart is limited by the spouse's earnings too - period: 2025 - input: - people: - person1: - age: 35 - employment_income: 38_000 - dependent_care_employer_benefits: 2_500 - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - filing_status: SEPARATE - absent_spouse_earned_income: 1_000 - output: - cdcc_treated_as_unmarried: false - # min($2,500 benefits, $2,500 MFS cap, min($38,000, $1,000)). - dependent_care_assistance_exclusion: 1_000 - -- name: A separated head of household is limited by the absent spouse's earnings - period: 2025 - input: - people: - person1: - age: 35 - employment_income: 38_000 - dependent_care_employer_benefits: 4_000 - is_separated: true - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - filing_status: HEAD_OF_HOUSEHOLD - absent_spouse_earned_income: 3_000 - output: - dependent_care_assistance_exclusion: 3_000 - -- name: Without the absent spouse's earnings, the filer's own earnings apply - period: 2025 - input: - people: - person1: - age: 35 - employment_income: 38_000 - dependent_care_employer_benefits: 4_000 - is_separated: true - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - filing_status: SEPARATE - output: - absent_spouse_earned_income: 38_000 - dependent_care_assistance_exclusion: 4_000 - -- name: An unmarried head of household is not limited by an absent spouse's earnings - period: 2025 - input: - people: - person1: - age: 35 - employment_income: 38_000 - dependent_care_employer_benefits: 4_000 - person2: - age: 5 - tax_units: - tax_unit: - members: [person1, person2] - filing_status: HEAD_OF_HOUSEHOLD - absent_spouse_earned_income: 0 - output: - dependent_care_assistance_exclusion: 4_000 diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py b/policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py deleted file mode 100644 index 1f2ed2478c2..00000000000 --- a/policyengine_us/variables/gov/irs/credits/cdcc/absent_spouse_earned_income.py +++ /dev/null @@ -1,28 +0,0 @@ -from policyengine_us.model_api import * - - -class absent_spouse_earned_income(Variable): - value_type = float - entity = TaxUnit - label = "Earned income of a married taxpayer's spouse who is not on the return" - documentation = ( - "For a taxpayer who is married at the end of the year but does not " - "file a joint return (a separate filer, or a separated filer who " - "files as head of household), the spouse's earned income for the " - "section 129(b)(1)(B) dependent care assistance limit, including any " - "amount the spouse is deemed to earn as a student or as incapable of " - "self-care under section 129(b)(2). The spouse is not a member of the " - "tax unit, so the model has no other record of these earnings. If it " - "is not supplied, it defaults to the earned income limit that applies " - "without it (min_head_spouse_earned, the taxpayer's own earnings on a " - "return that is not joint), which leaves the limit unchanged." - ) - unit = USD - definition_period = YEAR - reference = ( - "https://www.law.cornell.edu/uscode/text/26/129#b_1", - "https://www.law.cornell.edu/uscode/text/26/129#b_2", - ) - - def formula(tax_unit, period, parameters): - return tax_unit("min_head_spouse_earned", period) diff --git a/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py b/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py index a976b10f18b..3746d08563b 100644 --- a/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py +++ b/policyengine_us/variables/gov/irs/credits/cdcc/dependent_care_assistance_exclusion.py @@ -17,8 +17,6 @@ class dependent_care_assistance_exclusion(Variable): "https://www.law.cornell.edu/uscode/text/26/129#a_2", # Dollar cap. "https://www.law.cornell.edu/uscode/text/26/129#a_2_C", # Marital status. "https://www.law.cornell.edu/uscode/text/26/129#b", # Earned income limit. - # Form 2441 line 19 (spouse's earned income). - "https://www.irs.gov/pub/irs-prior/i2441--2025.pdf#page=5", # ARPA section 9632 temporarily increased the cap for 2021. "https://www.govinfo.gov/content/pkg/PLAW-117publ2/pdf/PLAW-117publ2.pdf#page=158", # Form 2441 Part III lines 12-26 compute the excluded benefit. @@ -45,31 +43,9 @@ def formula(tax_unit, period, parameters): p.reduction_amount[filing_status], ) # Section 129(b) earned-income limitation: the exclusion cannot exceed - # the taxpayer's earned income, or, for a taxpayer married at the end - # of the year, the lesser of the taxpayer's and the spouse's earned - # income (Form 2441 lines 18-19). min_head_spouse_earned returns the - # taxpayer's earnings when unmarried and the lesser of the two when - # married filing jointly. + # the taxpayer's earned income, or, for a married taxpayer, the lesser + # of the taxpayer's and spouse's earned income (Form 2441 lines 18-19). + # min_head_spouse_earned returns the taxpayer's earnings when unmarried + # and the lesser of the two when married filing jointly. earned_income_limit = tax_unit("min_head_spouse_earned", period) - # A married taxpayer who does not file jointly (a separate filer, or a - # separated filer filing as head of household) is still married for - # section 129(b)(1)(B). Section 129(a)(2)(C) imports the section - # 21(e)(4) rule for living apart "for purposes of this paragraph", - # the dollar cap, and section 129(b) does not import it. The Form 2441 - # line 19 instructions let a filer treated as unmarried under that - # rule enter their own earned income; the model follows the statute - # and uses the spouse's earned income, absent_spouse_earned_income. - separate = filing_status == filing_status.possible_values.SEPARATE - joint = filing_status == filing_status.possible_values.JOINT - person = tax_unit.members - separated = tax_unit.any( - person("is_tax_unit_head_or_spouse", period) - & person("is_separated", period) - ) - married_not_joint = separate | (separated & ~joint) - earned_income_limit = where( - married_not_joint, - min_(earned_income_limit, tax_unit("absent_spouse_earned_income", period)), - earned_income_limit, - ) return min_(benefits, min_(dollar_cap, earned_income_limit)) From de6830ab07f41834755f394b713f2804ab1db25c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:19:01 -0400 Subject: [PATCH 6/6] Document the EITC royalty basket and section 911 comments (review r1) Review r1 follow-ups: the 32(i)(2)(C) basket holds only estate and trust royalties because the model has no direct-royalty input; cite section 652(b) alongside 662(b); note the royalty input is not checked against estate_income; update the tax_unit_is_filer comment for the narrower 24(d)(3) bar; name ARPA section 9611 for former 24(i); and note the legacy section 911 aggregates in the changelog. The section 129(b) change moves to a decision (statute ambiguous on "married"). Co-Authored-By: Claude Opus 5.5 --- .../statute-first-federal-income-tax.fixed.md | 2 +- ...ble_ctc_barred_by_section_911_exclusion.py | 3 ++- .../eitc_relevant_investment_income.py | 21 ++++++++++++------- .../variables/gov/irs/tax_unit_is_filer.py | 8 ++++--- .../person/estate/estate_royalty_income.py | 8 ++++--- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/changelog.d/statute-first-federal-income-tax.fixed.md b/changelog.d/statute-first-federal-income-tax.fixed.md index 722ee77d1d4..cc2887ac6d6 100644 --- a/changelog.d/statute-first-federal-income-tax.fixed.md +++ b/changelog.d/statute-first-federal-income-tax.fixed.md @@ -1 +1 @@ -Follow the statute where IRS forms differ: the refundable child tax credit bar applies only to a section 911 exclusion (not the housing deduction alone), and a claimable spouse aged 65 or blind on a 2018-2025 joint return must file with income other than earned income over the section 63(c)(5)(A) amount. +Follow the statute where IRS forms differ: the refundable child tax credit bar applies only to a section 911 exclusion, not the housing deduction alone (with Form 2555 leaf inputs; a positive legacy aggregate still counts as an exclusion); a claimable spouse aged 65 or blind on a 2018-2025 joint return must file with income other than earned income over the section 63(c)(5)(A) amount; estate and trust royalties count as EITC investment income; and through 2020 former section 32(c)(1)(F) denies the EITC to a filer whose qualifying children all lack identification. diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py index 07d10092118..a35e48d6fa9 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py @@ -30,7 +30,8 @@ class refundable_ctc_barred_by_section_911_exclusion(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.irs.credits.ctc.refundable excludes_section_911 = excludes_amount_under_section_911(tax_unit, period) - # Section 24(i)(1)(A) (2021) switched off all of subsection (d), + # Former section 24(i)(1)(A) (2021, added by ARPA section 9611) + # switched off all of subsection (d), # including this bar, for filers whose principal place of abode was in # the United States for more than half the year and for bona fide # residents of Puerto Rico. The model assumes every filer meets that diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py b/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py index 9f0f2860665..ab6c5d57106 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py @@ -32,15 +32,21 @@ class eitc_relevant_investment_income(Variable): "Part III columns (e) and (f), such as Schedule K-1 (Form 1041) box 5 " "annuities and income in respect of a decedent) is not on lines 11 " "and 12. Box 5 royalties not derived in the ordinary course of a trade " - "or business (estate_royalty_income) are their own basket under 26 USC " - "32(i)(2)(C), net of their allocable deductions and floored at zero. " + "or business (estate_royalty_income) go in the 26 USC 32(i)(2)(C) " + "basket, net of their allocable deductions and floored at zero. " "Worksheet 1 line 8 takes royalties only from Schedule E line 23b, so " - "it leaves these out; the model follows the statute." + "it leaves these out; the model follows the statute. Section " + "32(i)(2)(C) nets all nonbusiness rents and royalties in one basket; " + "the model has no separate input for royalties received directly " + "(Schedule E Part I), so the basket holds only estate and trust " + "royalties, and the undifferentiated rental_income stays in the " + "passive basket." ) unit = USD definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/32#i_2", + "https://www.law.cornell.edu/uscode/text/26/652#b", "https://www.law.cornell.edu/uscode/text/26/662#b", "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=7", "https://www.irs.gov/pub/irs-prior/f8814--2025.pdf#page=1", @@ -120,10 +126,11 @@ def formula(tax_unit, period, parameters): tax_unit, period, ["eitc_passive_income_also_in_earned_income"] ) # Section 32(i)(2)(C): rents and royalties not derived in the ordinary - # course of a trade or business, net of their allocable deductions, in - # a basket of their own. A beneficiary's box 5 royalties keep that - # character (section 662(b)) and are nonpassive, so they are not in - # the passive basket. Worksheet 1 line 8 takes royalties only from + # course of a trade or business, net of their allocable deductions. + # The model has only estate and trust royalties for this basket (no + # input for direct royalties). A beneficiary's box 5 royalties keep + # that character (sections 652(b) and 662(b)) and are nonpassive, so + # they are not in the passive basket. Worksheet 1 line 8 takes royalties only from # Schedule E line 23b, leaving these out; the model follows the # statute. nonbusiness_royalties = tax_unit_non_dep_add( diff --git a/policyengine_us/variables/gov/irs/tax_unit_is_filer.py b/policyengine_us/variables/gov/irs/tax_unit_is_filer.py index d67b3372a6b..914c7002bd6 100644 --- a/policyengine_us/variables/gov/irs/tax_unit_is_filer.py +++ b/policyengine_us/variables/gov/irs/tax_unit_is_filer.py @@ -50,9 +50,11 @@ def formula(tax_unit, period, parameters): files_voluntarily = tax_unit("would_file_taxes_voluntarily", period) # A section 911 exclusion is claimed on Form 2555 attached to a - # return (Treas. Reg. 1.911-7(a)(1) and 1.6012-1(a)(3)). Sections - # 24(d)(3) and 32(c)(1)(C) deny the claimant the refundable credits, - # so those credits cannot be its reason to file. A claimant under the + # return (Treas. Reg. 1.911-7(a)(1) and 1.6012-1(a)(3)). Section + # 32(c)(1)(C) denies every claimant the EITC, and section 24(d)(3) + # denies the refundable CTC to one who elects an exclusion (not one + # who claims only the housing deduction); eligible_for_refundable_credits + # handles which credit can be a reason to file. A claimant under the # filing threshold need not file in a later year of a continuing # election, so the propensity to file for a tax benefit decides. claims_section_911 = elects_section_911_exclusion(tax_unit, period) diff --git a/policyengine_us/variables/household/income/person/estate/estate_royalty_income.py b/policyengine_us/variables/household/income/person/estate/estate_royalty_income.py index 2f19a5341d9..888844167c3 100644 --- a/policyengine_us/variables/household/income/person/estate/estate_royalty_income.py +++ b/policyengine_us/variables/household/income/person/estate/estate_royalty_income.py @@ -11,8 +11,8 @@ class estate_royalty_income(Variable): "ordinary course of a trade or business, net of the deductions and " "interest clearly and directly allocable to them: Schedule K-1 (Form " "1041) box 5 royalties, reported in Schedule E Part III column (f). " - "Section 662(b) gives a beneficiary's share of trust income the " - "character it had in the trust, so 26 USC 32(i)(2)(C) counts these " + "Sections 652(b) and 662(b) give a beneficiary's share of trust income " + "the character it had in the trust, so 26 USC 32(i)(2)(C) counts these " "royalties as disqualified income for the EITC. Publication 596 " "Worksheet 1 line 8 takes royalties only from Schedule E line 23b " "(Part I) and so leaves them out; the model follows the statute. " @@ -20,12 +20,14 @@ class estate_royalty_income(Variable): "amount. The default is zero. This classifies income already counted " "in estate_income and does not represent additional income; box 5 " "also holds annuities and income in respect of a decedent, which are " - "not royalties and are not included here." + "not royalties and are not included here. The input is not checked " + "against estate_income or passive_estate_income." ) definition_period = YEAR default_value = 0 reference = ( "https://www.law.cornell.edu/uscode/text/26/32#i_2_C", + "https://www.law.cornell.edu/uscode/text/26/652#b", "https://www.law.cornell.edu/uscode/text/26/662#b", "https://www.irs.gov/pub/irs-prior/p596--2025.pdf#page=7", "https://www.irs.gov/pub/irs-prior/f1041sk1--2025.pdf#page=2",