From 8641d4bde26067521a239888d182bfe83dae12ca Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 17:10:24 -0400 Subject: [PATCH 1/3] Count only elective deferrals of self-employed plan contributions in the saver's credit 26 U.S.C. 25B(d)(1)(B) counts elective deferrals as defined in 402(g)(3). Nonelective SEP and profit-sharing contributions, and matching contributions a self-employed person makes for themself (402(g)(8)), are employer contributions. Form 8880 line 2 (2025 and 2022, p. 2) and Pub. 590-A (2025 p. 47, 2022 p. 45) say the same. The new self_employed_pension_elective_deferrals counts the person's Schedule 1 line 16 contributions as deferrals first, up to the 402(g) limit plus catch-up less deferrals to other employers' 401(k)/403(b) plans, and replaces self_employed_pension_contributions in the saver's credit list. Co-Authored-By: Claude Opus 5.5 --- ...vers-credit-se-elective-deferrals.fixed.md | 1 + ...fied_retirement_savings_contributions.yaml | 20 ++-- .../savers_credit_person.yaml | 25 ++++ ...savers_credit_qualified_contributions.yaml | 65 +++++++++- ...f_employed_pension_elective_deferrals.yaml | 113 ++++++++++++++++++ ...elf_employed_pension_elective_deferrals.py | 50 ++++++++ 6 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 changelog.d/fix-savers-credit-se-elective-deferrals.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_elective_deferrals.yaml create mode 100644 policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py diff --git a/changelog.d/fix-savers-credit-se-elective-deferrals.fixed.md b/changelog.d/fix-savers-credit-se-elective-deferrals.fixed.md new file mode 100644 index 00000000000..fad1bcd4728 --- /dev/null +++ b/changelog.d/fix-savers-credit-se-elective-deferrals.fixed.md @@ -0,0 +1 @@ +Count only the elective deferral part of a self-employed person's own plan contributions toward the saver's credit (26 U.S.C. 25B(d)(1)(B)): contributions beyond the section 402(g) and catch-up room left by deferrals to other employers' plans are SEP, profit-sharing or matching contributions. diff --git a/policyengine_us/parameters/gov/irs/credits/retirement_saving/qualified_retirement_savings_contributions.yaml b/policyengine_us/parameters/gov/irs/credits/retirement_saving/qualified_retirement_savings_contributions.yaml index f7ea26c772b..b4d0ffc04fb 100644 --- a/policyengine_us/parameters/gov/irs/credits/retirement_saving/qualified_retirement_savings_contributions.yaml +++ b/policyengine_us/parameters/gov/irs/credits/retirement_saving/qualified_retirement_savings_contributions.yaml @@ -1,4 +1,4 @@ -description: The IRA sums the following elements as qualified retirement savings contributions when computing the saver's credit. +description: The IRS sums the following elements as qualified retirement savings contributions when computing the saver's credit. values: 2018-01-01: - traditional_ira_contributions @@ -7,7 +7,7 @@ values: - traditional_403b_contributions - roth_401k_contributions - roth_403b_contributions - - self_employed_pension_contributions + - self_employed_pension_elective_deferrals - able_contributions_person 2026-01-01: @@ -17,7 +17,7 @@ values: - traditional_403b_contributions - roth_401k_contributions - roth_403b_contributions - - self_employed_pension_contributions + - self_employed_pension_elective_deferrals metadata: unit: list @@ -26,7 +26,13 @@ metadata: reference: - title: 26 U.S. Code § 25B - (d) href: https://www.law.cornell.edu/uscode/text/26/25B#d_1 - - title: 2023 Publication 590-A - href: https://www.irs.gov/pub/irs-pdf/p590a.pdf#page=45 - - title: 2023 Form 8880 - href: https://www.irs.gov/pub/irs-pdf/f8880.pdf#page=2 + - title: 26 U.S. Code § 402(g)(3) - elective deferrals + href: https://www.law.cornell.edu/uscode/text/26/402#g_3 + - title: 2025 Publication 590-A, eligible contributions + href: https://www.irs.gov/pub/irs-prior/p590a--2025.pdf#page=47 + - title: 2022 Publication 590-A, eligible contributions + href: https://www.irs.gov/pub/irs-prior/p590a--2022.pdf#page=45 + - title: 2025 Form 8880 instructions, line 2 + href: https://www.irs.gov/pub/irs-prior/f8880--2025.pdf#page=2 + - title: 2022 Form 8880 instructions, line 2 + href: https://www.irs.gov/pub/irs-prior/f8880--2022.pdf#page=2 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_person.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_person.yaml index 11d92ee3b0d..ecca1f8c68a 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_person.yaml @@ -317,3 +317,28 @@ # 48,000 on the joint schedule (divided by 0.5), so the rate is 20%, # not the 50% that 22,000 (44,000) would give. savers_credit_person: 400 + +- name: Self-employed employer contributions beyond the elective deferral room do not count toward the credit + period: 2025 + input: + people: + person1: + age: 40 + employment_income: 30_000 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 20_000 + self_employed_pension_contributions_desired: 5_000 + retirement_distributions: 22_000 + tax_units: + tax_unit: + members: [person1] + filing_status: SINGLE + adjusted_gross_income: 20_000 + output: + # 26 U.S.C. 25B(d)(1)(B): the 401(k) deferrals use all $23,500 of + # elective deferral room, so the self-employed plan contributions are + # employer contributions. Qualified contributions are + # 23,500 + 0 - 22,000 = 1,500, and the 50% rate gives 750. Counting the + # $5,000 would give 6,500, capped at 2,000, and a credit of 1,000. + savers_credit_qualified_contributions: 1_500 + savers_credit_person: 750 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml index 672916a2ac5..9ae8476da63 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/retirement_saving/savers_credit_qualified_contributions.yaml @@ -33,11 +33,70 @@ output: savers_credit_qualified_contributions: 0 -- name: Self-employed pension contributions count after applying the annual additions cap +# 26 U.S.C. 25B(d)(1)(B) counts elective deferrals (402(g)(3)), not SEP, +# profit-sharing or matching contributions a self-employed person makes for +# themself. Self-employed plan contributions count only up to the elective +# deferral room the person's other deferrals leave +# (self_employed_pension_elective_deferrals). +- name: Self-employed plan contributions count only up to the elective deferral limit. period: 2026 input: + age: 40 self_employment_income: 100_000 self_employed_pension_contributions_desired: 100_000 output: - self_employed_pension_contributions: 72_000 - savers_credit_qualified_contributions: 72_000 + # The 2026 limit is $24,500. + self_employed_pension_elective_deferrals: 24_500 + savers_credit_qualified_contributions: 24_500 + +- name: Self-employed plan contributions beyond the room left by employer-plan deferrals do not count. + period: 2025 + input: + age: 40 + employment_income: 60_000 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 5_000 + output: + # The 401(k) deferrals use all $23,500 of room. + savers_credit_qualified_contributions: 23_500 + +- name: Self-employed plan contributions count up to the room left by employer-plan deferrals. + period: 2025 + input: + age: 40 + employment_income: 60_000 + ira_compensation: 10_000 + roth_ira_contributions_desired: 2_000 + traditional_403b_contributions_desired: 21_000 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 4_000 + output: + # 21,000 + (23,500 - 21,000) + 2,000 of IRA contributions. + self_employed_pension_elective_deferrals: 2_500 + savers_credit_qualified_contributions: 25_500 + +- name: Self-employed plan contributions count up to the catch-up room at age 55. + period: 2025 + input: + age: 55 + employment_income: 60_000 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 10_000 + output: + # 23,500 + 7,500 of catch-up room. + savers_credit_qualified_contributions: 31_000 + +- name: Self-employed employer contributions do not offset distributions. + period: 2025 + input: + age: 40 + employment_income: 60_000 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 5_000 + retirement_distributions: 25_000 + output: + # 23,500 + 0 - 25,000, floored at 0. + savers_credit_qualified_contributions: 0 diff --git a/policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_elective_deferrals.yaml b/policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_elective_deferrals.yaml new file mode 100644 index 00000000000..44d25f10e78 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/expense/retirement/self_employed_pension_elective_deferrals.yaml @@ -0,0 +1,113 @@ +# The part of the person's own self-employed plan contributions that can be +# elective deferrals (26 U.S.C. 402(g)(3)): contributions count as deferrals +# first, up to the section 402(g) limit plus the catch-up amount less the +# person's deferrals to other employers' 401(k) and 403(b) plans. Published +# limits: $20,500 + $6,500 (2022), $23,500 + $7,500 or $11,250 at ages 60 to +# 63 (2025), $24,500 + $8,000 (2026). Each case's contributions are within the +# self-employed plan contribution limit, so they equal the desired amount. + +- name: Case 1, no other deferrals, so all contributions can be deferrals. + period: 2025 + input: + age: 40 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 5_000 + output: + self_employed_pension_contributions: 5_000 + self_employed_pension_elective_deferrals: 5_000 + +- name: Case 2, deferrals to an employer's plan fill the limit, so none can be deferrals. + period: 2025 + input: + age: 40 + employment_income: 60_000 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 5_000 + output: + self_employed_pension_contributions: 5_000 + # 23,500 - 23,500 = 0 of room. + self_employed_pension_elective_deferrals: 0 + +- name: Case 3, partial room after deferrals to an employer's plan. + period: 2025 + input: + age: 40 + employment_income: 60_000 + traditional_401k_contributions_desired: 22_000 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 3_000 + output: + # 23,500 - 22,000 = 1,500 of room. + self_employed_pension_elective_deferrals: 1_500 + +- name: Case 4, catch-up room at age 55. + period: 2025 + input: + age: 55 + employment_income: 60_000 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 10_000 + output: + # 23,500 + 7,500 - 23,500 = 7,500 of room. + self_employed_pension_elective_deferrals: 7_500 + +- name: Case 5, the higher catch-up at age 61. + period: 2025 + input: + age: 61 + employment_income: 60_000 + traditional_401k_contributions_desired: 23_500 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 15_000 + output: + # 23,500 + 11,250 - 23,500 = 11,250 of room. + self_employed_pension_elective_deferrals: 11_250 + +- name: Case 6, Roth 403(b) and traditional 401(k) deferrals share the room. + period: 2026 + input: + age: 40 + employment_income: 60_000 + traditional_401k_contributions_desired: 10_000 + roth_403b_contributions_desired: 10_000 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 8_000 + output: + # 24,500 - 20,000 = 4,500 of room. + self_employed_pension_elective_deferrals: 4_500 + +- name: Case 7, desired employer-plan deferrals above the limit still leave no room. + period: 2025 + input: + age: 40 + employment_income: 60_000 + traditional_401k_contributions_desired: 30_000 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 5_000 + output: + # Deferrals are scaled to 23,500, leaving no room. + traditional_401k_contributions: 23_500 + self_employed_pension_elective_deferrals: 0 + +- name: Case 8, 2022 room. + period: 2022 + input: + age: 40 + employment_income: 60_000 + roth_401k_contributions_desired: 20_000 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 2_000 + output: + # 20,500 - 20,000 = 500 of room. + self_employed_pension_elective_deferrals: 500 + +- name: Case 9, no self-employed plan contributions. + period: 2025 + input: + age: 40 + self_employment_income: 100_000 + self_employed_pension_contributions_desired: 0 + output: + self_employed_pension_elective_deferrals: 0 diff --git a/policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py b/policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py new file mode 100644 index 00000000000..5731354a58b --- /dev/null +++ b/policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py @@ -0,0 +1,50 @@ +from policyengine_us.model_api import * + + +class self_employed_pension_elective_deferrals(Variable): + value_type = float + entity = Person + label = "Self-employed plan elective deferrals" + unit = USD + documentation = ( + "Part of the person's own self-employed SEP, SIMPLE and qualified " + "plan contributions (Schedule 1 line 16) that is elective deferrals " + "under section 402(g)(3). Nonelective SEP and profit-sharing " + "contributions, and the matching contributions a self-employed " + "person makes for themself (section 402(g)(8)), are employer " + "contributions, not elective deferrals. The modeled input does not " + "say which plan the person has, so contributions count as elective " + "deferrals first, up to the section 402(g) limit plus the person's " + "catch-up amount less their deferrals to other employers' 401(k) and " + "403(b) plans; the rest are employer contributions. This is the most " + "of the contributions that can be elective deferrals. For a SEP or a " + "profit-sharing plan without a 401(k) arrangement, supply 0." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/402#g_1", + "https://www.law.cornell.edu/uscode/text/26/402#g_3", + "https://www.law.cornell.edu/uscode/text/26/402#g_8", + "https://www.law.cornell.edu/uscode/text/26/414#v_3_A", + "https://www.irs.gov/pub/irs-prior/f8880--2025.pdf#page=2", + "https://www.irs.gov/pub/irs-prior/p590a--2025.pdf#page=47", + ) + + def formula(person, period, parameters): + contributions = person("self_employed_pension_contributions", period) + # Section 402(g)(1) limits a person's elective deferrals across all + # plans, so deferrals to other employers' plans use the room first. + other_deferrals = add( + person, + period, + [ + "traditional_401k_contributions", + "roth_401k_contributions", + "traditional_403b_contributions", + "roth_403b_contributions", + ], + ) + deferral_room = max_( + person("elective_deferral_limit", period) - other_deferrals, 0 + ) + return min_(contributions, deferral_room) From 2668300cccaeefcac851f9704c8ecb6927d87902 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 17:13:16 -0400 Subject: [PATCH 2/3] Add property tests for the saver's credit self-employed deferral base Co-Authored-By: Claude Opus 5.5 --- ...redit_self_employed_deferral_properties.py | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 policyengine_us/tests/unit/test_savers_credit_self_employed_deferral_properties.py diff --git a/policyengine_us/tests/unit/test_savers_credit_self_employed_deferral_properties.py b/policyengine_us/tests/unit/test_savers_credit_self_employed_deferral_properties.py new file mode 100644 index 00000000000..a6060985c7f --- /dev/null +++ b/policyengine_us/tests/unit/test_savers_credit_self_employed_deferral_properties.py @@ -0,0 +1,235 @@ +"""Properties of the saver's credit base for self-employed plan contributions. + +26 U.S.C. 25B(d)(1)(B) counts elective deferrals (section 402(g)(3)) toward the +saver's credit. Nonelective SEP and profit-sharing contributions, and the +matching contributions a self-employed person makes for themself (section +402(g)(8)), are employer contributions and do not count. The model's input, +`self_employed_pension_contributions`, is the person's whole Schedule 1 line 16 +contribution and does not say which plan the person has, so +`self_employed_pension_elective_deferrals` counts it as elective deferrals +first, up to the section 402(g) room the person's deferrals to other +employers' 401(k) and 403(b) plans leave (the limit plus the person's catch-up +amount under section 414(v)). + +The reference below uses the IRS's published limits rather than model +parameters. It takes from the model only amounts this change does not touch: +the IRA contributions and the line 16 contribution after its own limit. +Hypothesis draws batches of single filers across three years, and each batch +runs as one vectorized simulation. For every person and year: + +1. The counted deferrals equal min(line 16 contribution, room), with + 0 <= counted <= line 16 contribution and counted <= room. +2. The person's elective deferrals across all plans stay within the 402(g) + limit plus catch-up. +3. The credit base equals IRA contributions + employer-plan deferrals + the + counted deferrals - distributions, floored at 0. It never exceeds the base + that counts the whole line 16 contribution, and falls short of it by at + most the line 16 contribution that is not counted. +4. The person's credit is between 0 and 50% of the base capped at $2,000. + +A deterministic grid checks that the counted deferrals rise with the line 16 +contribution and fall as deferrals to an employer's plan use up the room. +""" + +import itertools + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 +# Published section 402(g)(1) limits and section 414(v) catch-up amounts: IRS +# Notices 2021-61 (2022), 2024-80 (2025) and 2025-67 (2026); section +# 414(v)(2)(E) raises the catch-up for ages 60 to 63 from 2025. +PUBLISHED = { + 2022: dict(deferral=20_500, catch_up=6_500, catch_up_60_to_63=6_500), + 2025: dict(deferral=23_500, catch_up=7_500, catch_up_60_to_63=11_250), + 2026: dict(deferral=24_500, catch_up=8_000, catch_up_60_to_63=11_250), +} +YEARS = list(PUBLISHED) +# Section 25B(a) and (b): the highest credit rate and the per-person cap on +# contributions. +TOP_RATE = 0.5 +CONTRIBUTION_CAP = 2_000 +EMPLOYER_PLAN_DEFERRALS = [ + "traditional_401k_contributions_desired", + "roth_401k_contributions_desired", + "traditional_403b_contributions_desired", + "roth_403b_contributions_desired", +] + + +def deferral_limit(year, age): + amounts = PUBLISHED[year] + catch_up = np.select( + [age < 50, (age >= 60) & (age <= 63)], + [0, amounts["catch_up_60_to_63"]], + amounts["catch_up"], + ) + return amounts["deferral"] + catch_up + + +def every_year(value): + return {year: value for year in YEARS} + + +def build_situation(people_inputs): + people, tax_units, households = {}, {}, {} + for i, inputs in enumerate(people_inputs): + name = f"p{i}" + people[name] = { + variable: every_year(float(value)) for variable, value in inputs.items() + } + tax_units[f"t{i}"] = {"members": [name]} + households[f"h{i}"] = {"members": [name], "state_code": every_year("TX")} + return {"people": people, "tax_units": tax_units, "households": households} + + +def column(people_inputs, variable): + return np.array([p.get(variable, 0) for p in people_inputs], dtype=float) + + +def check(sim, people_inputs): + age = column(people_inputs, "age") + desired_employer_plan = sum( + column(people_inputs, variable) for variable in EMPLOYER_PLAN_DEFERRALS + ) + distributions = column(people_inputs, "retirement_distributions") + for year in YEARS: + message = f"year {year}" + limit = deferral_limit(year, age) + # Deferrals to employers' plans, scaled down to the limit when above it. + employer_plan = np.minimum(desired_employer_plan, limit) + room = limit - employer_plan + line_16 = sim.calculate("self_employed_pension_contributions", year) + counted = sim.calculate("self_employed_pension_elective_deferrals", year) + ira = sim.calculate("traditional_ira_contributions", year) + sim.calculate( + "roth_ira_contributions", year + ) + model_employer_plan = sum( + sim.calculate(variable.removesuffix("_desired"), year) + for variable in EMPLOYER_PLAN_DEFERRALS + ) + base = sim.calculate("savers_credit_qualified_contributions", year) + credit = sim.calculate("savers_credit_person", year) + + # 1. The counted deferrals. + assert np.all(line_16 >= 0), message + np.testing.assert_allclose( + model_employer_plan, employer_plan, atol=TOLERANCE, err_msg=message + ) + np.testing.assert_allclose( + counted, np.minimum(line_16, room), atol=TOLERANCE, err_msg=message + ) + assert np.all(counted >= -TOLERANCE), message + assert np.all(counted <= line_16 + TOLERANCE), message + assert np.all(counted <= room + TOLERANCE), message + # 2. Section 402(g) across all of the person's plans. + assert np.all(model_employer_plan + counted <= limit + TOLERANCE), message + # 3. The credit base. + expected_base = np.maximum(ira + employer_plan + counted - distributions, 0) + np.testing.assert_allclose(base, expected_base, atol=TOLERANCE, err_msg=message) + whole_line_16_base = np.maximum( + ira + employer_plan + line_16 - distributions, 0 + ) + assert np.all(base <= whole_line_16_base + TOLERANCE), message + assert np.all(whole_line_16_base - base <= line_16 - counted + TOLERANCE), ( + message + ) + # 4. The credit. + assert np.all(credit >= -TOLERANCE), message + assert np.all( + credit <= TOP_RATE * np.minimum(base, CONTRIBUTION_CAP) + TOLERANCE + ), message + + +profits = st.one_of( + st.sampled_from([0, 1_000, 20_000, 100_000, 400_000]), + st.integers(-20_000, 500_000), +) +deferrals = st.one_of( + st.just(0), + st.sampled_from([20_500, 23_500, 24_500, 31_000, 34_750]), + st.integers(0, 40_000), +) +person_strategy = st.fixed_dictionaries( + { + "age": st.sampled_from([30, 49, 50, 55, 59, 60, 61, 63, 64, 70]), + "employment_income": st.one_of(st.just(0), st.integers(0, 200_000)), + "self_employment_income": profits, + "self_employed_pension_contributions_desired": st.one_of( + st.sampled_from([0, 1_000, 1_000_000]), st.integers(0, 100_000) + ), + "traditional_401k_contributions_desired": deferrals, + "roth_401k_contributions_desired": st.one_of( + st.just(0), st.integers(0, 25_000) + ), + "traditional_403b_contributions_desired": st.one_of( + st.just(0), st.integers(0, 25_000) + ), + "roth_403b_contributions_desired": st.one_of( + st.just(0), st.integers(0, 25_000) + ), + "ira_compensation": st.one_of(st.just(0), st.integers(0, 20_000)), + "traditional_ira_contributions_desired": st.one_of( + st.just(0), st.integers(0, 8_000) + ), + "roth_ira_contributions_desired": st.one_of(st.just(0), st.integers(0, 8_000)), + "retirement_distributions": st.one_of(st.just(0), st.integers(0, 60_000)), + } +) + +SETTINGS = dict( + max_examples=20, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.lists(person_strategy, min_size=4, max_size=15)) +def test_credit_counts_only_the_elective_deferral_room(people_inputs): + sim = Simulation(situation=build_situation(people_inputs)) + check(sim, people_inputs) + + +GRID_EMPLOYER_PLAN = [0, 10_000, 20_000, 23_500, 40_000] +GRID_LINE_16 = [0, 1_000, 5_000, 20_000, 60_000] + + +@pytest.fixture(scope="module") +def grid(): + people_inputs = [ + { + "age": 55, + "employment_income": 80_000, + "self_employment_income": 300_000, + "traditional_401k_contributions_desired": employer_plan, + "self_employed_pension_contributions_desired": line_16, + } + for employer_plan, line_16 in itertools.product( + GRID_EMPLOYER_PLAN, GRID_LINE_16 + ) + ] + sim = Simulation(situation=build_situation(people_inputs)) + return sim, people_inputs + + +@pytest.mark.parametrize("year", YEARS) +def test_grid_is_monotone(grid, year): + sim, people_inputs = grid + check(sim, people_inputs) + shape = len(GRID_EMPLOYER_PLAN), len(GRID_LINE_16) + counted = sim.calculate("self_employed_pension_elective_deferrals", year).reshape( + shape + ) + # The counted deferrals rise with the line 16 contribution... + assert np.all(np.diff(counted, axis=1) >= -TOLERANCE) + # ...and fall as deferrals to an employer's plan use up the room. + assert np.all(np.diff(counted, axis=0) <= TOLERANCE) + # With deferrals of $40,000 to an employer's plan, no room is left. + np.testing.assert_allclose(counted[-1], 0, atol=TOLERANCE) From e3f8425422adda45439901b04245a599c848c299 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 17:20:38 -0400 Subject: [PATCH 3/3] Cite sections 402A(a)(1) and 408(p)(9) and Pub. 560 step 20 for the deferral share Co-Authored-By: Claude Opus 5.5 --- .../self_employed_pension_elective_deferrals.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py b/policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py index 5731354a58b..45514def57c 100644 --- a/policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py +++ b/policyengine_us/variables/household/expense/retirement/self_employed_pension_elective_deferrals.py @@ -10,11 +10,13 @@ class self_employed_pension_elective_deferrals(Variable): "Part of the person's own self-employed SEP, SIMPLE and qualified " "plan contributions (Schedule 1 line 16) that is elective deferrals " "under section 402(g)(3). Nonelective SEP and profit-sharing " - "contributions, and the matching contributions a self-employed " - "person makes for themself (section 402(g)(8)), are employer " - "contributions, not elective deferrals. The modeled input does not " - "say which plan the person has, so contributions count as elective " - "deferrals first, up to the section 402(g) limit plus the person's " + "contributions, and the 401(k) or SIMPLE IRA matching contributions " + "a self-employed person makes for themself (sections 402(g)(8) and " + "408(p)(9)), are employer contributions, not elective deferrals. " + "The line 16 deduction excludes designated Roth deferrals, so they " + "are not modeled here. The modeled contribution does not say which " + "plan the person has, so it counts as elective deferrals first, up " + "to the section 402(g) limit plus the person's " "catch-up amount less their deferrals to other employers' 401(k) and " "403(b) plans; the rest are employer contributions. This is the most " "of the contributions that can be elective deferrals. For a SEP or a " @@ -25,9 +27,11 @@ class self_employed_pension_elective_deferrals(Variable): "https://www.law.cornell.edu/uscode/text/26/402#g_1", "https://www.law.cornell.edu/uscode/text/26/402#g_3", "https://www.law.cornell.edu/uscode/text/26/402#g_8", + "https://www.law.cornell.edu/uscode/text/26/408#p_9", "https://www.law.cornell.edu/uscode/text/26/414#v_3_A", "https://www.irs.gov/pub/irs-prior/f8880--2025.pdf#page=2", "https://www.irs.gov/pub/irs-prior/p590a--2025.pdf#page=47", + "https://www.irs.gov/pub/irs-prior/p560--2025.pdf#page=39", ) def formula(person, period, parameters):