From 72b76cb2581c39cea2dd8372fc335dd9f15fb562 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 18:05:46 -0400 Subject: [PATCH 1/3] Apply the Form 4952 election in Schedule D Tax Worksheet lines 3-10 dwks09 and dividend_income_reduced_by_investment_income read investment_income_form_4952, an input no dataset fills (California's FTB 3526 line 9 reads it as federal Form 4952 line 8), and applied it as min(0, election) and with line 4 assumed zero. net_capital_gain read investment_income_elected_form_4952 (Form 4952 line 4g, populated in the default dataset). With a $50,000 election against a $300,000 long-term gain, net_capital_gain was $250,000 but dwks10 $300,000. - Worksheet lines 3, 4 and 7 come from one helper: line 3 is Form 4952 line 4g, line 4 is line 4e (all capital assets treated as held for investment), line 7 the smaller of Schedule D lines 15 and 16, with capital gain distributions on Schedule D line 13. - dividend_income_reduced_by_investment_income is line 6, dwks09 line 9 and dwks10 line 10 (lines 6 + 9). - net_capital_gain takes the election from the gain first and then from qualified dividends (Form 4952 line 4g instructions; 26 U.S.C. 1(h)(2) and 1(h)(11)(D)(i)), and nets capital gain distributions with Schedule D gains and losses. - adjusted_net_capital_gain adds back only the qualified dividends not elected. - dwks09 no longer drops Schedule D gains when capital gain distributions are also present. Tests: worksheet YAML cases with line comments, and Hypothesis differential tests (worksheet transcription, dwks10 == net_capital_gain, dwks13 == adjusted_net_capital_gain, monotonicity in the election). Co-Authored-By: Claude Opus 5.5 --- changelog.d/dwks-form-4952-election.fixed.md | 1 + .../schedule_d_tax_worksheet_form_4952.yaml | 293 ++++++++++++++++++ .../test_form_4952_election_worksheet.py | 273 ++++++++++++++++ .../sources/investment_income_form_4952.py | 10 + .../adjusted_net_capital_gain.py | 15 +- ...end_income_reduced_by_investment_income.py | 33 +- .../federal_income/capital_gains/dwks09.py | 37 ++- .../federal_income/capital_gains/dwks10.py | 33 +- .../capital_gains/net_capital_gain.py | 66 ++-- ...chedule_d_tax_worksheet_form_4952_lines.py | 52 ++++ .../investment_income_elected_form_4952.py | 15 + pyproject.toml | 1 + uv.lock | 90 ++++++ 13 files changed, 853 insertions(+), 66 deletions(-) create mode 100644 changelog.d/dwks-form-4952-election.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml create mode 100644 policyengine_us/tests/test_form_4952_election_worksheet.py create mode 100644 policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py diff --git a/changelog.d/dwks-form-4952-election.fixed.md b/changelog.d/dwks-form-4952-election.fixed.md new file mode 100644 index 00000000000..204c68fac76 --- /dev/null +++ b/changelog.d/dwks-form-4952-election.fixed.md @@ -0,0 +1 @@ +Apply the Form 4952 line 4g investment income election in the Schedule D Tax Worksheet (lines 3 to 10) as in net capital gain, take any election above the gain from qualified dividends (26 U.S.C. 1(h)(11)(D)(i)), and count capital gain distributions together with Schedule D gains in worksheet line 9. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml new file mode 100644 index 00000000000..920dd366fb3 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml @@ -0,0 +1,293 @@ +# 2025 Schedule D Tax Worksheet lines 2-10 (Instructions for Schedule D, page +# 15) with a Form 4952 line 4g election: +# 2. qualified dividends +# 3. Form 4952 line 4g (the amount elected to include in investment income) +# 4. Form 4952 line 4e (the smaller of line 4d, the net gain from investment +# property, or the net capital gain from investment property) +# 5. line 3 - line 4, not below zero +# 6. line 2 - line 5, not below zero +# 7. the smaller of Schedule D line 15 or line 16 +# 8. the smaller of line 3 or line 4 +# 9. line 7 - line 8, not below zero +# 10. line 6 + line 9 +# Line 6 is dividend_income_reduced_by_investment_income, line 9 dwks09 and +# line 10 dwks10. Line 10 equals net_capital_gain, which applies 26 U.S.C. +# 1(h)(2) and 1(h)(11)(D)(i) to the same inputs. + +- name: An election within the gain comes off the gain + period: 2025 + input: + people: + person1: + long_term_capital_gains: 300_000 + investment_income_elected_form_4952: 50_000 + tax_units: + tax_unit: + members: [person1] + output: + # Line 2 = 0; line 3 = 50,000; line 4 = min(300,000, 300,000) = 300,000; + # line 5 = 0; line 6 = 0. + dividend_income_reduced_by_investment_income: 0 + # Line 7 = 300,000; line 8 = min(50,000, 300,000) = 50,000; + # line 9 = 300,000 - 50,000. + dwks09: 250_000 + # Line 10 = 0 + 250,000. + dwks10: 250_000 + net_capital_gain: 250_000 + +- name: An election above the gain comes off the qualified dividends + period: 2025 + input: + people: + person1: + long_term_capital_gains: 20_000 + qualified_dividend_income: 30_000 + investment_income_elected_form_4952: 35_000 + tax_units: + tax_unit: + members: [person1] + output: + # Line 4 = 20,000; line 5 = 35,000 - 20,000 = 15,000; + # line 6 = 30,000 - 15,000. + dividend_income_reduced_by_investment_income: 15_000 + # Line 7 = 20,000; line 8 = min(35,000, 20,000) = 20,000; line 9 = 0. + dwks09: 0 + # Line 10 = 15,000 + 0. + dwks10: 15_000 + net_capital_gain: 15_000 + +- name: An election above the gain and the dividends leaves nothing + period: 2025 + input: + people: + person1: + long_term_capital_gains: 10_000 + qualified_dividend_income: 5_000 + investment_income_elected_form_4952: 20_000 + tax_units: + tax_unit: + members: [person1] + output: + # Line 5 = 20,000 - 10,000 = 10,000; line 6 = 5,000 - 10,000, so zero. + dividend_income_reduced_by_investment_income: 0 + # Line 8 = 10,000; line 9 = 10,000 - 10,000 = 0. + dwks09: 0 + dwks10: 0 + net_capital_gain: 0 + +- name: A short-term loss lowers lines 4 and 7 + period: 2025 + input: + people: + person1: + long_term_capital_gains: 40_000 + short_term_capital_gains: -10_000 + qualified_dividend_income: 5_000 + investment_income_elected_form_4952: 32_000 + tax_units: + tax_unit: + members: [person1] + output: + # Form 4952 line 4d = 40,000 - 10,000 = 30,000; net capital gain from + # investment property = 40,000 - 10,000 = 30,000; line 4 = 30,000. + # Line 5 = 32,000 - 30,000 = 2,000; line 6 = 5,000 - 2,000. + dividend_income_reduced_by_investment_income: 3_000 + # Schedule D line 15 = 40,000, line 16 = 30,000; line 7 = 30,000; + # line 8 = min(32,000, 30,000) = 30,000; line 9 = 0. + dwks09: 0 + dwks10: 3_000 + net_capital_gain: 3_000 + +- name: A short-term gain leaves line 4 at the net capital gain + period: 2025 + input: + people: + person1: + long_term_capital_gains: 40_000 + short_term_capital_gains: 15_000 + investment_income_elected_form_4952: 10_000 + tax_units: + tax_unit: + members: [person1] + output: + # Form 4952 line 4d = 55,000; net capital gain from investment property + # = 40,000 (no short-term loss); line 4 = 40,000. Line 5 = 0. + dividend_income_reduced_by_investment_income: 0 + # Schedule D line 15 = 40,000, line 16 = 55,000; line 7 = 40,000; + # line 8 = 10,000; line 9 = 30,000. + dwks09: 30_000 + dwks10: 30_000 + net_capital_gain: 30_000 + +- name: Capital gain distributions without Schedule D can be elected + period: 2025 + input: + people: + person1: + non_sch_d_capital_gains: 8_000 + qualified_dividend_income: 2_000 + investment_income_elected_form_4952: 3_000 + tax_units: + tax_unit: + members: [person1] + output: + # Distributions belong on Schedule D line 13 and are long-term gains for + # Form 4952, so lines 4, 15 and 16 are 8,000. Line 5 = 0; line 6 = 2,000. + dividend_income_reduced_by_investment_income: 2_000 + # Line 7 = 8,000; line 8 = 3,000; line 9 = 5,000. + dwks09: 5_000 + dwks10: 7_000 + net_capital_gain: 7_000 + +- name: Capital gain distributions net against a long-term loss + period: 2025 + input: + people: + person1: + long_term_capital_gains: -5_000 + non_sch_d_capital_gains: 3_000 + tax_units: + tax_unit: + members: [person1] + output: + # Schedule D line 15 = -5,000 + 3,000 = -2,000 = line 16; line 7 = + # -2,000; line 9 = 0. No net capital gain under 26 U.S.C. 1222(11). + dwks09: 0 + dwks10: 0 + net_capital_gain: 0 + +- name: Distributions and Schedule D gains both count + period: 2025 + input: + people: + person1: + long_term_capital_gains: 12_000 + non_sch_d_capital_gains: 3_000 + qualified_dividend_income: 1_000 + tax_units: + tax_unit: + members: [person1] + output: + # Schedule D line 15 = 12,000 + 3,000 = 15,000 = line 16; line 9 = + # 15,000; line 10 = 1,000 + 15,000. + dwks09: 15_000 + dwks10: 16_000 + net_capital_gain: 16_000 + +- name: Without an election line 10 is the gain plus the dividends + period: 2025 + input: + people: + person1: + long_term_capital_gains: 10_000 + short_term_capital_gains: -1_000 + qualified_dividend_income: 2_000 + tax_units: + tax_unit: + members: [person1] + output: + dividend_income_reduced_by_investment_income: 2_000 + # Line 7 = min(10,000, 9,000) = 9,000; line 8 = 0. + dwks09: 9_000 + dwks10: 11_000 + net_capital_gain: 11_000 + +- name: The election is summed across the tax unit + period: 2025 + input: + people: + person1: + long_term_capital_gains: 60_000 + investment_income_elected_form_4952: 10_000 + person2: + qualified_dividend_income: 8_000 + investment_income_elected_form_4952: 55_000 + tax_units: + tax_unit: + members: [person1, person2] + output: + # Line 3 = 65,000; line 4 = 60,000; line 5 = 5,000; line 6 = 3,000. + dividend_income_reduced_by_investment_income: 3_000 + # Line 7 = 60,000; line 8 = 60,000; line 9 = 0. + dwks09: 0 + dwks10: 3_000 + net_capital_gain: 3_000 + +- name: Adjusted net capital gain and worksheet line 13 with an election + period: 2025 + input: + people: + person1: + long_term_capital_gains: 100_000 + qualified_dividend_income: 10_000 + investment_income_elected_form_4952: 30_000 + tax_units: + tax_unit: + members: [person1] + unrecaptured_section_1250_gain: 20_000 + output: + # Line 9 = 100,000 - 30,000 = 70,000; line 10 = 80,000; line 11 = + # 20,000; line 12 = min(70,000, 20,000); line 13 = 80,000 - 20,000. + dwks13: 60_000 + # 26 U.S.C. 1(h)(3): (70,000 - 20,000) + 10,000. + adjusted_net_capital_gain: 60_000 + +- name: Elected dividends are not added back to adjusted net capital gain + period: 2025 + input: + people: + person1: + long_term_capital_gains: 20_000 + qualified_dividend_income: 30_000 + investment_income_elected_form_4952: 35_000 + tax_units: + tax_unit: + members: [person1] + unrecaptured_section_1250_gain: 5_000 + output: + # Line 9 = 0; line 10 = 15,000; line 11 = 5,000; line 12 = 0; + # line 13 = 15,000. + dwks13: 15_000 + # 26 U.S.C. 1(h)(3) and (11)(D)(i): no gain left, plus the 15,000 of + # qualified dividends not elected. + adjusted_net_capital_gain: 15_000 + +- name: The elected gain is taxed at ordinary rates, also in Form 6251 Part III + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + long_term_capital_gains: 300_000 + investment_income_elected_form_4952: 50_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + # Single, 2025. Schedule D Tax Worksheet: line 1 = 300,000 - 15,750 = + # 284,250; line 10 = 250,000; line 13 = 250,000; line 14 = 34,250. + # Lines 15-22: 48,350 - 34,250 = 14,100 at 0%. Lines 23-31: line 25 = + # 250,000 - 14,100 = 235,900, line 29 = 284,250 - 48,350 = 235,900, and + # 15% of 235,900 = 35,385. Line 44: tax on line 21 (34,250) = 1,192.50 + + # 2,679 = 3,871.50. Line 45 = 39,256.50, less than line 46 (69,034.75). + # Before the fix line 10 was 300,000 and line 14 zero. + taxable_income: 284_250 + dwks13: 250_000 + dwks14: 34_250 + # The model splits line 45 into the tax at the main rates and the + # section 1(h) capital gains tax. + income_tax_main_rates: 3_871.50 + capital_gains_tax: 35_385 + # Form 6251 Part III: line 12 = 300,000 - 88,100 = 211,900; line 13 = + # 250,000; line 15 = min(250,000 + 0, line 10 = 250,000); line 16 = + # 211,900; line 17 = 0. Line 20 = worksheet line 14 = 34,250; line 21 = + # 48,350 - 34,250 = 14,100; line 22 = 211,900; line 23 = 14,100 at 0%; + # line 24 = 197,800. Line 28 = 14,100 + worksheet line 21 (34,250) = + # 48,350; line 29 = 533,400 - 48,350 = 485,050; line 30 = 197,800; line + # 31 = 29,670; line 38 = 29,670. Before the fix: 24,532.50. + amt_tax_including_cg: 29_670 diff --git a/policyengine_us/tests/test_form_4952_election_worksheet.py b/policyengine_us/tests/test_form_4952_election_worksheet.py new file mode 100644 index 00000000000..e73185087d8 --- /dev/null +++ b/policyengine_us/tests/test_form_4952_election_worksheet.py @@ -0,0 +1,273 @@ +"""Differential and property tests for the Form 4952 line 4g election. + +A taxpayer may elect to treat net capital gain and qualified dividends as +investment income under 26 U.S.C. 163(d)(4)(B). The model applies the +election twice: + +- `net_capital_gain` follows the statute: section 1(h)(2) removes the gain + taken into account under section 163(d)(4)(B)(iii), and section + 1(h)(11)(D)(i) removes elected dividends from qualified dividend income. +- `dividend_income_reduced_by_investment_income`, `dwks09` and `dwks10` + follow lines 2 to 10 of the 2025 Schedule D Tax Worksheet (Instructions + for Schedule D, page 15). + +Both attribute the election first to net capital gain and then to qualified +dividends, as the 2025 Form 4952 line 4g instructions do by default. These +tests check that the two routes, and a line-by-line transcription of the +worksheet below, agree for every combination of gains, losses, capital gain +distributions, dividends and elections. + +Amounts are whole dollars of at most $2,000,000, so every sum the model forms +is below 2**24 and exact in single precision, and the comparisons are exact. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation + +# Hypothesis is a dev extra; skip rather than fail collection without it. +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +YEAR = 2025 + +# --------------------------------------------------------------------------- +# The worksheet and Form 4952 line 4, transcribed. +# --------------------------------------------------------------------------- + + +def form_4952_line_4e(h): + """2025 Form 4952 lines 4d and 4e with every capital asset held for + investment. Line 4d: "the excess, if any, of your total gains over your + total losses ... include capital gain distributions". Line 4e: "the + smaller of line 4d or your net capital gain from the disposition of + property held for investment", which is "the excess, if any, of your net + long-term capital gain over your net short-term capital loss"; capital + gain distributions "are treated as long-term capital gains".""" + long_term = h["long_term"] + h["distributions"] + line_4d = max(0, long_term + h["short_term"]) + net_capital_gain = max(0, long_term - max(0, -h["short_term"])) + return min(line_4d, net_capital_gain) + + +def schedule_d_tax_worksheet(h): + """2025 Schedule D Tax Worksheet lines 2 to 10. Returns lines 6, 9 and 10. + + Capital gain distributions go on Schedule D line 13, so they are part of + lines 15 and 16. + """ + schedule_d_line_15 = h["long_term"] + h["distributions"] + schedule_d_line_16 = schedule_d_line_15 + h["short_term"] + line_2 = h["dividends"] + line_3 = h["election"] + line_4 = form_4952_line_4e(h) + line_5 = max(0, line_3 - line_4) + line_6 = max(0, line_2 - line_5) + line_7 = min(schedule_d_line_15, schedule_d_line_16) + line_8 = min(line_3, line_4) + line_9 = max(0, line_7 - line_8) + line_10 = line_6 + line_9 + return line_6, line_9, line_10 + + +# --------------------------------------------------------------------------- +# The model. +# --------------------------------------------------------------------------- + +OUTPUTS = [ + "dividend_income_reduced_by_investment_income", + "dwks09", + "dwks10", + "dwks13", + "net_capital_gain", + "adjusted_net_capital_gain", +] + + +def build_situation(households): + """One single-person tax unit per household. The election and the + dividends are split between spouses when `split` is set, to check that + the model sums them over the tax unit.""" + people, tax_units, households_out = {}, {}, {} + for i, h in enumerate(households): + head = { + "age": {YEAR: 45}, + "long_term_capital_gains": {YEAR: h["long_term"]}, + "short_term_capital_gains": {YEAR: h["short_term"]}, + "non_sch_d_capital_gains": {YEAR: h["distributions"]}, + "qualified_dividend_income": {YEAR: h["dividends"]}, + "investment_income_elected_form_4952": {YEAR: h["election"]}, + } + members = [f"head_{i}"] + if h["split"]: + half_dividends = h["dividends"] // 2 + half_election = h["election"] // 2 + head["qualified_dividend_income"] = {YEAR: h["dividends"] - half_dividends} + head["investment_income_elected_form_4952"] = { + YEAR: h["election"] - half_election + } + people[f"spouse_{i}"] = { + "age": {YEAR: 45}, + "qualified_dividend_income": {YEAR: half_dividends}, + "investment_income_elected_form_4952": {YEAR: half_election}, + } + members.append(f"spouse_{i}") + people[f"head_{i}"] = head + tax_units[f"tax_unit_{i}"] = { + "members": members, + "unrecaptured_section_1250_gain": {YEAR: h["section_1250"]}, + "capital_gains_28_percent_rate_gain": {YEAR: h["rate_gain_28"]}, + } + households_out[f"household_{i}"] = { + "members": members, + "state_code": {YEAR: "TX"}, + } + return {"people": people, "tax_units": tax_units, "households": households_out} + + +def calculate(households): + simulation = Simulation(situation=build_situation(households)) + return {v: np.asarray(simulation.calculate(v, YEAR)) for v in OUTPUTS} + + +def assert_worksheet_equals_statute(households): + model = calculate(households) + for i, h in enumerate(households): + line_6, line_9, line_10 = schedule_d_tax_worksheet(h) + assert model["dividend_income_reduced_by_investment_income"][i] == line_6, h + assert model["dwks09"][i] == line_9, h + assert model["dwks10"][i] == line_10, h + # The statute route reaches worksheet line 10. + assert model["net_capital_gain"][i] == line_10, h + # Section 1(h)(3) against worksheet line 13: line 10 less the smaller + # of line 9 or line 11 (Schedule D lines 18 and 19). + line_11 = h["section_1250"] + h["rate_gain_28"] + line_13 = line_10 - min(line_9, line_11) + assert model["dwks13"][i] == line_13, h + assert model["adjusted_net_capital_gain"][i] == line_13, h + # Bounds: the election never adds gain, never removes more than it + # elects, and never takes line 10 below zero. + before_election = h["dividends"] + max( + 0, + min(h["long_term"], h["long_term"] + h["short_term"]) + h["distributions"], + ) + assert 0 <= line_10 <= before_election, h + assert line_10 >= before_election - h["election"], h + return model + + +# --------------------------------------------------------------------------- +# Strategies. Bounds keep every model sum below 2**24. +# --------------------------------------------------------------------------- + +DOLLARS = 2_000_000 + + +@st.composite +def households(draw): + long_term = draw( + st.one_of( + st.just(0), + st.integers(-DOLLARS // 4, DOLLARS), + ) + ) + short_term = draw(st.one_of(st.just(0), st.integers(-DOLLARS // 4, DOLLARS // 4))) + distributions = draw(st.one_of(st.just(0), st.integers(0, DOLLARS // 8))) + dividends = draw(st.one_of(st.just(0), st.integers(0, DOLLARS))) + gain = max( + 0, min(long_term + distributions, long_term + distributions + short_term) + ) + # Elections near the gain and near the gain plus dividends are the + # boundaries of the worksheet's min and max steps. + election = draw( + st.one_of( + st.just(0), + st.integers(0, DOLLARS), + st.sampled_from([gain, gain + dividends]), + st.integers(max(0, gain - 5), gain + dividends + 5), + ) + ) + return { + "long_term": long_term, + "short_term": short_term, + "distributions": distributions, + "dividends": dividends, + "election": election, + "section_1250": draw(st.one_of(st.just(0), st.integers(0, DOLLARS // 4))), + "rate_gain_28": draw(st.one_of(st.just(0), st.integers(0, DOLLARS // 4))), + "split": draw(st.booleans()), + } + + +GRID = [ + { + "long_term": long_term, + "short_term": short_term, + "distributions": distributions, + "dividends": dividends, + "election": election, + "section_1250": section_1250, + "rate_gain_28": 0, + "split": split, + } + for long_term, short_term in ( + (0, 0), + (300_000, 0), + (40_000, -10_000), + (40_000, 15_000), + (-5_000, 0), + (5_000, -20_000), + ) + for distributions in (0, 3_000) + for dividends in (0, 30_000) + for election in (0, 2_000, 35_000, 50_000, 400_000) + for section_1250, split in ((0, False), (20_000, True)) +] + + +def test_user_example(): + """A $50,000 election against a $300,000 long-term gain gives worksheet + lines 9 and 10 of $250,000, and the same net capital gain.""" + h = { + "long_term": 300_000, + "short_term": 0, + "distributions": 0, + "dividends": 0, + "election": 50_000, + "section_1250": 0, + "rate_gain_28": 0, + "split": False, + } + assert schedule_d_tax_worksheet(h) == (0, 250_000, 250_000) + model = assert_worksheet_equals_statute([h]) + assert model["dwks10"][0] == 250_000 + assert model["net_capital_gain"][0] == 250_000 + + +def test_grid(): + model = assert_worksheet_equals_statute(GRID) + # The grid reaches each branch: an election inside the gain, one that + # spills into dividends, and one that exceeds both. + assert (model["dwks10"] > 0).any() and (model["dwks10"] == 0).any() + reduced = model["dividend_income_reduced_by_investment_income"] + dividends = np.array([h["dividends"] for h in GRID]) + assert ((reduced > 0) & (reduced < dividends)).any() + + +@hypothesis.settings(max_examples=40, deadline=None, derandomize=True) +@hypothesis.given(st.lists(households(), min_size=1, max_size=25)) +def test_worksheet_equals_statute(batch): + assert_worksheet_equals_statute(batch) + + +@hypothesis.settings(max_examples=15, deadline=None, derandomize=True) +@hypothesis.given(households(), st.integers(0, DOLLARS)) +def test_more_election_never_raises_line_10(h, more): + """Line 10 is non-increasing in the election and falls by at most the + extra amount elected.""" + larger = dict(h, election=h["election"] + more) + model = calculate([h, larger]) + assert model["dwks10"][1] <= model["dwks10"][0] + assert model["dwks10"][0] - model["dwks10"][1] <= more + assert np.array_equal(model["dwks10"], model["net_capital_gain"]) diff --git a/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py b/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py index 40ddc613990..ca5e537beb2 100644 --- a/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py +++ b/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py @@ -5,5 +5,15 @@ class investment_income_form_4952(Variable): value_type = float entity = TaxUnit label = "Investment income from Form 4952" + documentation = ( + "The federal Form 4952 amount that California form FTB 3526 line 9 " + "asks for (federal Form 4952 line 8). The federal Schedule D Tax " + "Worksheet does not read this input; it takes the Form 4952 line 4g " + "election from investment_income_elected_form_4952." + ) unit = USD definition_period = YEAR + reference = dict( + title="2021 California form FTB 3526, line 9", + href="https://www.ftb.ca.gov/forms/2021/2021-3526.pdf", + ) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py index 109837b4226..98c3c7bfae1 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py @@ -24,13 +24,18 @@ def formula(tax_unit, period, parameters): # definition here, but it's a good enough approximation. See 26 U.S. Code § 1(h)(11)(B) for the # definition of 'net capital gain' for the above variable, and 26 U.S. Code § 1(h)(3) for the definition # of adjusted net capital gain (this variable). - qualified_dividend_income = add(tax_unit, period, ["qualified_dividend_income"]) + # Qualified dividend income as defined in paragraph (11), which + # excludes dividends elected as investment income (§ 1(h)(11)(D)(i)). + # net_capital_gain takes a Form 4952 election from the gain before + # the dividends, so the dividends it still holds are the smaller of + # the two amounts. + qualified_dividend_income = min_( + max_(0, add(tax_unit, period, ["qualified_dividend_income"])), + net_capital_gain, + ) unrecaptured_s_1250_gain = tax_unit("unrecaptured_section_1250_gain", period) cg_28_pct_rate_gain = tax_unit("capital_gains_28_percent_rate_gain", period) - net_gains_less_dividends = max_( - 0, - net_capital_gain - qualified_dividend_income, - ) + net_gains_less_dividends = net_capital_gain - qualified_dividend_income reduced_capital_gains = max_( net_gains_less_dividends - (unrecaptured_s_1250_gain + cg_28_pct_rate_gain), 0, diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py index 7c83c19d68a..c1942cc2082 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.schedule_d_tax_worksheet_form_4952_lines import ( + schedule_d_tax_worksheet_form_4952_lines, +) class dividend_income_reduced_by_investment_income(Variable): @@ -6,12 +9,30 @@ class dividend_income_reduced_by_investment_income(Variable): entity = TaxUnit definition_period = YEAR label = "Dividend income reduced by investment income" - documentation = "IRS Form 1040 Schedule D worksheet (part 1 of 6)" + documentation = ( + "Schedule D Tax Worksheet line 6: qualified dividends less the part " + "of any Form 4952 line 4g election not attributed to net capital " + "gain. Qualified dividend income after 26 U.S.C. 1(h)(11)(D)(i)." + ) unit = USD + reference = [ + dict( + title="2025 Instructions for Schedule D, Schedule D Tax Worksheet, lines 2-6", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=15", + ), + dict( + title="26 U.S. Code § 1(h)(11)(D)(i)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_11_D_i", + ), + ] def formula(tax_unit, period, parameters): - qualified_dividend_income = add(tax_unit, period, ["qualified_dividend_income"]) - investment_income = tax_unit("investment_income_form_4952", period) - # dwks04 always assumed to be zero - floored_investment_income = max_(0, investment_income) - return max_(0, qualified_dividend_income - floored_investment_income) + # Line 2: qualified dividends (Form 1040 line 3a). + line_2 = add(tax_unit, period, ["qualified_dividend_income"]) + lines = schedule_d_tax_worksheet_form_4952_lines(tax_unit, period) + # Line 5: line 3 minus line 4; if zero or less, zero. The part of the + # election that net capital gain from investment property cannot + # cover comes from qualified dividends. + line_5 = max_(0, lines.line_3 - lines.line_4) + # Line 6: line 2 minus line 5; if zero or less, zero. + return max_(0, line_2 - line_5) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks09.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks09.py index 1b339163d15..c36f008fac5 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks09.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks09.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.schedule_d_tax_worksheet_form_4952_lines import ( + schedule_d_tax_worksheet_form_4952_lines, +) class dwks09(Variable): @@ -6,18 +9,28 @@ class dwks09(Variable): entity = TaxUnit definition_period = YEAR label = "IRS Form 1040 Schedule D worksheet (part 2 of 6)" + documentation = ( + "Schedule D Tax Worksheet line 9: the smaller of Schedule D line 15 " + "or 16, less the part of any Form 4952 line 4g election attributed " + "to net capital gain. Net capital gain other than qualified " + "dividends, after 26 U.S.C. 1(h)(2)." + ) unit = USD - reference = "https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf" + reference = [ + dict( + title="2025 Instructions for Schedule D, Schedule D Tax Worksheet, lines 3-9", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=15", + ), + dict( + title="26 U.S. Code § 1(h)(2)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_2", + ), + ] def formula(tax_unit, period, parameters): - # Schedule D lines 15 and 16 contain capital gains, not qualified - # dividends. Dividends enter separately in dwks10; including them - # here double counts them when short-term gains are positive. - long_term_gains = add(tax_unit, period, ["long_term_capital_gains"]) - net_cg = min_(long_term_gains, tax_unit("net_capital_gains", period)) - other_cg = add(tax_unit, period, ["non_sch_d_capital_gains"]) - mod_cg = where(other_cg > 0, other_cg, max_(0, net_cg) + other_cg) - return max_( - 0, - mod_cg - min_(0, tax_unit("investment_income_form_4952", period)), - ) + lines = schedule_d_tax_worksheet_form_4952_lines(tax_unit, period) + # Line 8: the smaller of line 3 or line 4, the part of the Form 4952 + # line 4g election that comes from net capital gain. + line_8 = min_(lines.line_3, lines.line_4) + # Line 9: line 7 minus line 8; if zero or less, zero. + return max_(0, lines.line_7 - line_8) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks10.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks10.py index 3cd875d37c5..f5e9aafdbf6 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks10.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks10.py @@ -6,27 +6,14 @@ class dwks10(Variable): entity = TaxUnit definition_period = YEAR label = "IRS Form 1040 Schedule D worksheet (part 3 of 6)" + documentation = ( + "Schedule D Tax Worksheet line 10: lines 6 and 9, the qualified " + "dividends and net capital gain left after any Form 4952 line 4g " + "election. Equals net_capital_gain." + ) unit = USD - - def formula(tax_unit, period, parameters): - dwks10_if_gains = add( - tax_unit, - period, - ["dividend_income_reduced_by_investment_income", "dwks09"], - ) - dwks10_if_no_gains = max_( - 0, - min_( - add( - tax_unit, - period, - ["long_term_capital_gains", "qualified_dividend_income"], - ), - tax_unit("net_capital_gains", period), - ), - ) + add(tax_unit, period, ["non_sch_d_capital_gains"]) - return where( - tax_unit("has_qdiv_or_ltcg", period), - dwks10_if_gains, - dwks10_if_no_gains, - ) + reference = dict( + title="2025 Instructions for Schedule D, Schedule D Tax Worksheet, line 10", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=15", + ) + adds = ["dividend_income_reduced_by_investment_income", "dwks09"] diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py index b05d62b053d..d6011f76864 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py @@ -7,32 +7,58 @@ class net_capital_gain(Variable): label = "Net capital gain" unit = USD documentation = ( - "The excess of net long-term capital gain over net short-term capital" - 'loss, plus qualified dividends (the definition of "net capital gain"' - "which applies to 26 U.S.C. § 1(h) from § 1(h)(11))." + "The excess of net long-term capital gain over net short-term capital " + "loss, reduced by any part of it elected as investment income, plus " + "qualified dividends not elected as investment income (the " + 'definition of "net capital gain" which applies to 26 U.S.C. § 1(h), ' + "from § 1(h)(2) and § 1(h)(11))." ) definition_period = YEAR - reference = dict( - title="26 U.S. Code § 1222(11)", - href="https://www.law.cornell.edu/uscode/text/26/1222#11", - ) + reference = [ + dict( + title="26 U.S. Code § 1222(11)", + href="https://www.law.cornell.edu/uscode/text/26/1222#11", + ), + dict( + title="26 U.S. Code § 1(h)(2)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_2", + ), + dict( + title="26 U.S. Code § 1(h)(11)(A), (D)(i)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_11", + ), + dict( + title="2025 Form 4952, line 4g instructions", + href="https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=4", + ), + ] def formula(tax_unit, period, parameters): - lt_capital_gain = max_(0, add(tax_unit, period, ["long_term_capital_gains"])) - st_capital_loss = max_(0, -add(tax_unit, period, ["short_term_capital_gains"])) - investment_income_election = add( + # Capital gain distributions, including those reported without + # Schedule D (Form 1040 line 7 with the box checked), are long-term + # capital gains under IRC 852(b)(3)(B). + long_term_gain = add( tax_unit, period, - ["investment_income_elected_form_4952"], + ["long_term_capital_gains", "non_sch_d_capital_gains"], + ) + short_term_loss = max_(0, -add(tax_unit, period, ["short_term_capital_gains"])) + # 26 U.S.C. 1222(11), determined without regard to section 1(h)(11). + gain = max_(0, long_term_gain - short_term_loss) + # Form 4952 line 4g: the net capital gain and qualified dividends + # elected to be included in investment income (26 U.S.C. 163(d)(4)(B)). + # The amount "is generally treated as being attributable first to net + # capital gain ... and then to qualified dividends" (line 4g + # instructions). Section 1(h)(2) removes the gain part, the amount + # taken into account under 163(d)(4)(B)(iii); section 1(h)(11)(D)(i) + # removes the rest from qualified dividend income. + election = max_( + 0, add(tax_unit, period, ["investment_income_elected_form_4952"]) ) - net_cap_gain = max_( + gain_elected = min_(election, gain) + dividends_elected = election - gain_elected + qualified_dividends = max_( 0, - lt_capital_gain - st_capital_loss - investment_income_election, + add(tax_unit, period, ["qualified_dividend_income"]) - dividends_elected, ) - qual_div_income = add(tax_unit, period, ["qualified_dividend_income"]) - # Capital gain distributions reported without Schedule D (Form 1040 - # line 7 with the box checked) are long-term gains under IRC - # 852(b)(3)(B) and enter the preferential-rate base directly, with no - # Schedule D netting available on that filing path. - non_sch_d_capital_gains = add(tax_unit, period, ["non_sch_d_capital_gains"]) - return net_cap_gain + qual_div_income + non_sch_d_capital_gains + return gain - gain_elected + qualified_dividends diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py new file mode 100644 index 00000000000..f3eb7b9d7c7 --- /dev/null +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py @@ -0,0 +1,52 @@ +from collections import namedtuple + +from policyengine_us.model_api import * + +ScheduleDTaxWorksheetForm4952Lines = namedtuple( + "ScheduleDTaxWorksheetForm4952Lines", ["line_3", "line_4", "line_7"] +) + + +def schedule_d_tax_worksheet_form_4952_lines(tax_unit, period): + """Schedule D Tax Worksheet lines 3, 4 and 7 (2025 Instructions for + Schedule D, page 15), shared by dwks09 and + dividend_income_reduced_by_investment_income. + + Line 3 is Form 4952 line 4g, the net capital gain and qualified dividends + the taxpayer elects to include in investment income (26 U.S.C. + 163(d)(4)(B)). Line 4 is Form 4952 line 4e, "the smaller of line 4d or + your net capital gain from the disposition of property held for + investment". Line 7 is "the smaller of line 15 or line 16 of Schedule D". + + The model treats every capital asset as property held for investment, so + Form 4952 line 4d is the excess of total gains over total losses and + line 4e's net capital gain is the excess of net long-term capital gain + over net short-term capital loss (2025 Form 4952 instructions, lines 4d + and 4e). It has no input for the "Elec." amount a taxpayer may write next + to line 4e (the worksheet's line 4 footnote), so the election is + attributed first to net capital gain and then to qualified dividends, as + the line 4g instructions do by default. + + Capital gain distributions reported without Schedule D + (non_sch_d_capital_gains) belong on Schedule D line 13 (2025 Instructions + for Schedule D, "Capital Gain Distributions"), so they enter lines 15 and + 16, and Form 4952 counts them as long-term capital gains. + """ + election = max_(0, add(tax_unit, period, ["investment_income_elected_form_4952"])) + long_term_gains = add(tax_unit, period, ["long_term_capital_gains"]) + short_term_gains = add(tax_unit, period, ["short_term_capital_gains"]) + distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) + # Schedule D line 15 (net long-term gain or loss, with line 13) and line + # 16 (Schedule D line 7 plus line 15). + schedule_d_line_15 = long_term_gains + distributions + schedule_d_line_16 = tax_unit("net_capital_gains", period) + distributions + # Form 4952 line 4d: total gains over total losses, if any. + net_gain = max_(0, schedule_d_line_16) + # Net capital gain from property held for investment: net long-term + # capital gain over net short-term capital loss, if any. + net_capital_gain = max_(0, schedule_d_line_15 - max_(0, -short_term_gains)) + return ScheduleDTaxWorksheetForm4952Lines( + line_3=election, + line_4=min_(net_gain, net_capital_gain), + line_7=min_(schedule_d_line_15, schedule_d_line_16), + ) diff --git a/policyengine_us/variables/household/income/person/misc/investment_income_elected_form_4952.py b/policyengine_us/variables/household/income/person/misc/investment_income_elected_form_4952.py index b716a3864d5..a9a877b7a38 100644 --- a/policyengine_us/variables/household/income/person/misc/investment_income_elected_form_4952.py +++ b/policyengine_us/variables/household/income/person/misc/investment_income_elected_form_4952.py @@ -5,5 +5,20 @@ class investment_income_elected_form_4952(Variable): value_type = float entity = Person label = "investment income elected on Form 4952" + documentation = ( + "Form 4952 line 4g: net capital gain and qualified dividends the " + "taxpayer elects to include in investment income, which then lose " + "the capital gains tax rates. Schedule D Tax Worksheet line 3." + ) unit = USD definition_period = YEAR + reference = [ + dict( + title="26 U.S. Code § 163(d)(4)(B)", + href="https://www.law.cornell.edu/uscode/text/26/163#d_4_B", + ), + dict( + title="2025 Form 4952, line 4g", + href="https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", + ), + ] diff --git a/pyproject.toml b/pyproject.toml index ada6b6fa3ca..40ba2d3950f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "setuptools>=80.9.0", "build>=1.2.2.post1", "towncrier>=24.8.0", + "hypothesis>=6.100.0", ] [tool.hatch.build.targets.wheel] diff --git a/uv.lock b/uv.lock index b84171deda0..4f169a24605 100644 --- a/uv.lock +++ b/uv.lock @@ -766,6 +766,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/07/3d0c34c345043c6a398a5882e196b2220dc5861adfa18322448b90908f26/huggingface_hub-1.3.4-py3-none-any.whl", hash = "sha256:a0c526e76eb316e96a91e8a1a7a93cf66b0dd210be1a17bd5fc5ae53cba76bfd", size = 536611, upload-time = "2026-01-26T14:05:08.549Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, + { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, + { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1746,6 +1834,7 @@ dev = [ { name = "build" }, { name = "coverage" }, { name = "furo" }, + { name = "hypothesis" }, { name = "jupyter-book" }, { name = "ruff" }, { name = "setuptools" }, @@ -1757,6 +1846,7 @@ requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2.post1" }, { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.9.2" }, { name = "furo", marker = "extra == 'dev'", specifier = ">=2024.8.6" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.100.0" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=1.0.4.post1" }, { name = "microdf-python", specifier = ">=1.0.0" }, { name = "pandas", specifier = ">=3.0" }, From 9a158d2ded2f8c376453484f9ae65e19ffd39252 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 22:32:06 -0400 Subject: [PATCH 2/3] Address review: label the Tax Table rounding, fix stale labels - The 2025 tax case's line 44 uses the Tax Computation Worksheet rates the model uses; the Tax Table gives 3,875 (review r1, GPT-6.1 Sol). - investment_income_form_4952's label now matches its only use, federal Form 4952 line 8 for California FTB 3526 line 9 (review r1, Opus). - adjusted_net_capital_gain's documentation describes section 1(h)(3). Co-Authored-By: Claude Opus 5.5 --- .../capital_gains/schedule_d_tax_worksheet_form_4952.yaml | 4 +++- .../gov/irs/income/sources/investment_income_form_4952.py | 2 +- .../federal_income/capital_gains/adjusted_net_capital_gain.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml index 920dd366fb3..55bc9ed9711 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml @@ -274,7 +274,9 @@ # Lines 15-22: 48,350 - 34,250 = 14,100 at 0%. Lines 23-31: line 25 = # 250,000 - 14,100 = 235,900, line 29 = 284,250 - 48,350 = 235,900, and # 15% of 235,900 = 35,385. Line 44: tax on line 21 (34,250) = 1,192.50 + - # 2,679 = 3,871.50. Line 45 = 39,256.50, less than line 46 (69,034.75). + # 2,679 = 3,871.50 at the Tax Computation Worksheet rates, which the + # model uses (the Tax Table gives 3,875). Line 45 = 39,256.50, less + # than line 46 (69,034.75). # Before the fix line 10 was 300,000 and line 14 zero. taxable_income: 284_250 dwks13: 250_000 diff --git a/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py b/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py index ca5e537beb2..8551304d487 100644 --- a/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py +++ b/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py @@ -4,7 +4,7 @@ class investment_income_form_4952(Variable): value_type = float entity = TaxUnit - label = "Investment income from Form 4952" + label = "Federal Form 4952 line 8 amount for California FTB 3526" documentation = ( "The federal Form 4952 amount that California form FTB 3526 line 9 " "asks for (federal Form 4952 line 8). The federal Schedule D Tax " diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py index 98c3c7bfae1..e43194e2cb6 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py @@ -7,7 +7,9 @@ class adjusted_net_capital_gain(Variable): label = "Adjusted net capital gain" unit = USD documentation = ( - "The excess of net long-term capital gain over net short-term capital loss." + "Net capital gain other than qualified dividends, reduced by " + "unrecaptured section 1250 gain and 28-percent rate gain, plus " + "qualified dividends not elected as investment income." ) definition_period = YEAR reference = dict( From ea06980c865262a2d29450b4d2be7da780674ca0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 22:05:19 -0400 Subject: [PATCH 3/3] Section 911 amounts use qualified dividends after the Form 4952 election With dwks10 and net_capital_gain now applying the same election, the worksheet and section 1(h) capital gain excesses agree; update the docstrings that said they could disagree. section_911_qualified_dividend_income starts from qualified dividend income after 26 U.S.C. 1(h)(11)(D)(i) (the smaller of the dividends and net_capital_gain), so elected dividends are not added back in section_911_adjusted_net_capital_gain. Co-Authored-By: Claude Opus 5.5 --- .../federal_income/capital_gains/dwks14.yaml | 4 ++-- .../amt_section_911_capital_gain_excess.yaml | 4 ++-- ...section_911_qualified_dividend_income.yaml | 14 +++++++++++++ ...tax_worksheet_after_capital_gain_excess.py | 9 +++++--- .../section_911_capital_gain_excess.py | 6 +++--- .../section_911_qualified_dividend_income.py | 21 +++++++++++++++---- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks14.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks14.yaml index 2f9bd240d75..d5a2dfe83af 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks14.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks14.yaml @@ -46,8 +46,8 @@ # when neither capital gains worksheet was completed. dwks14: 130_000 -# Synthetic inputs: on a real return worksheet line 10 and net capital gain -# are the same amount. They are set apart here only to show which one each +# Synthetic inputs: on a real return, and in the model for the same inputs, +# worksheet line 10 and net capital gain are the same amount. They are set apart here only to show which one each # excess reads. - name: The worksheet lines read the excess from line 10, not net_capital_gain period: 2025 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/amt_section_911_capital_gain_excess.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/amt_section_911_capital_gain_excess.yaml index eee942d4233..acfe1108323 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/amt_section_911_capital_gain_excess.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/amt_section_911_capital_gain_excess.yaml @@ -28,8 +28,8 @@ output: amt_section_911_capital_gain_excess: 0 -# Synthetic inputs: on a real return worksheet line 10 and net capital gain -# are the same amount. They are set apart here only to show which one the +# Synthetic inputs: on a real return, and in the model for the same inputs, +# worksheet line 10 and net capital gain are the same amount. They are set apart here only to show which one the # AMT excess reads. - name: The AMT excess reads worksheet line 10, not net_capital_gain period: 2025 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.yaml index c4ca9605f72..f2b4771e3e4 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.yaml @@ -38,3 +38,17 @@ section_911_capital_gain_excess: 0 output: section_911_qualified_dividend_income: 10_000 + +- name: Dividends elected on Form 4952 are not qualified dividend income + period: 2025 + input: + qualified_dividend_income: 30_000 + # A 35,000 election against 20,000 of gain leaves 15,000 of dividends. + net_capital_gain: 15_000 + section_911_capital_gain_excess: 5_000 + output: + # 26 U.S.C. 1(h)(11)(D)(i): the dividends are 15,000, with no gain other + # than dividends; the whole excess comes off them: 15,000 - 5,000. + section_911_qualified_dividend_income: 10_000 + section_911_net_capital_gain: 10_000 + section_911_adjusted_net_capital_gain: 10_000 diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/schedule_d_tax_worksheet_after_capital_gain_excess.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/schedule_d_tax_worksheet_after_capital_gain_excess.py index 1aab78c733f..4580d5ca61f 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/schedule_d_tax_worksheet_after_capital_gain_excess.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/schedule_d_tax_worksheet_after_capital_gain_excess.py @@ -35,9 +35,12 @@ def schedule_d_tax_worksheet_after_capital_gain_excess(tax_unit, period, taxable The excess is measured as the worksheets measure it, from line 10. The section 1(h) formulas use section_911_capital_gain_excess, measured from - net_capital_gain. On a return the two are the same amount; in the model - dwks10 and net_capital_gain are separate formulas that can disagree (they - read different Form 4952 inputs). + net_capital_gain. On a return the two are the same amount, and in the + model dwks10 equals net_capital_gain for the same inputs: both apply the + Form 4952 line 4g election, first to the gain and then to qualified + dividends. With amounts in cents they can differ by single-precision + rounding, because worksheet lines 6 and 9 are stored before dwks10 adds + them. Returns the excess, worksheet lines 9, 10 and 13 and Schedule D line 19. Without an excess these are the first worksheet's amounts, unchanged. diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py index a024c8c3860..1cc7a77763a 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_capital_gain_excess.py @@ -15,9 +15,9 @@ class section_911_capital_gain_excess(Variable): "at the capital gains rates. The section 1(h) formulas use this " "amount; the Schedule D Tax Worksheet lines (dwks14, dwks19, " "regular_tax_before_credits) measure the excess from worksheet line " - "10, as the worksheet does. On a return the two are the same amount; " - "in the model dwks10 and net_capital_gain are separate formulas that " - "can disagree." + "10, as the worksheet does. On a return the two are the same amount, " + "and in the model dwks10 equals net_capital_gain for the same inputs, " + "apart from single-precision rounding." ) definition_period = YEAR reference = [ diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py index 1864f9fb7f3..3cfc0d25094 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py @@ -7,14 +7,19 @@ class section_911_qualified_dividend_income(Variable): label = "Qualified dividends less the section 911 capital gain excess" unit = USD documentation = ( - "Qualified dividend income for the regular tax rates. A taxpayer with " + "Qualified dividend income for the regular tax rates, without " + "dividends elected as investment income on Form 4952. A taxpayer with " "a section 911 capital gain excess reduces qualified dividends by the " "part of the excess that exceeds net capital gain other than " - "qualified dividends. Equals qualified dividend income for everyone " - "else." + "qualified dividends. Equals that qualified dividend income for " + "everyone else." ) definition_period = YEAR reference = [ + dict( + title="26 U.S. Code § 1(h)(11)(D)(i)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_11_D_i", + ), dict( title="26 U.S. Code § 911(f)(2)(A)(ii)", href="https://www.law.cornell.edu/uscode/text/26/911#f_2_A_ii", @@ -26,8 +31,16 @@ class section_911_qualified_dividend_income(Variable): ] def formula(tax_unit, period, parameters): - qualified_dividends = add(tax_unit, period, ["qualified_dividend_income"]) net_capital_gain = tax_unit("net_capital_gain", period) + # Qualified dividend income as section 1(h)(11) defines it, without + # dividends elected as investment income (§ 1(h)(11)(D)(i)). + # net_capital_gain takes a Form 4952 election from the gain before the + # dividends, so the dividends it still holds are the smaller of the + # two amounts. + qualified_dividends = min_( + max_(0, add(tax_unit, period, ["qualified_dividend_income"])), + net_capital_gain, + ) excess = tax_unit("section_911_capital_gain_excess", period) # Net capital gain determined without regard to section 1(h)(11). gain_other_than_dividends = max_(0, net_capital_gain - qualified_dividends)