diff --git a/changelog.d/fix-filing-requirement-dependents.fixed.md b/changelog.d/fix-filing-requirement-dependents.fixed.md new file mode 100644 index 00000000000..48846578166 --- /dev/null +++ b/changelog.d/fix-filing-requirement-dependents.fixed.md @@ -0,0 +1 @@ +Stop testing every filer's unearned income against $500 in the federal filing requirement (26 U.S.C. 6012): the unearned income test now applies only to a head or spouse who can be claimed as a dependent on another return, uses the section 63(c)(5)(A) amount for the year ($1,350 in 2025) plus that person's own additional standard deduction for age or blindness, and counts that person's gross income other than wages, business income and farm income (the earned income of the IRS dependent worksheet), so a self-employment loss no longer counts as unearned income. diff --git a/policyengine_us/parameters/gov/irs/income/filing_requirement/dependent/earned_income_sources.yaml b/policyengine_us/parameters/gov/irs/income/filing_requirement/dependent/earned_income_sources.yaml new file mode 100644 index 00000000000..22410a45918 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/income/filing_requirement/dependent/earned_income_sources.yaml @@ -0,0 +1,24 @@ +description: The Internal Revenue Service counts these gross income sources as earned income when it tests the unearned income of a filer who can be claimed as a dependent; every other gross income source is unearned income. +values: + 2010-01-01: + # Form 1040 line 1z (wages). + - irs_employment_income + # Schedule 1 line 3 (business income, Schedule C). + - self_employment_income + - sstb_self_employment_income + # Schedule 1 line 6 (farm income, Schedule F). + - farm_operations_income +metadata: + unit: list + period: year + label: IRS earned income sources for the dependent unearned income filing test + reference: + - title: 26 U.S. Code ยง 6012(a)(1)(C)(i)(I) and (f)(2)(D) - income other than earned income + href: https://www.law.cornell.edu/uscode/text/26/6012 + # "Generally, your earned income is the total of the amount(s) you + # reported on Form 1040 or 1040-SR, line 1z, and Schedule 1, lines 3, 6, + # 8r, 8t, and 8u minus the amount, if any, on Schedule 1, line 15." + # Schedule 1 line 5 (rents, royalties, partnerships, S corporations and + # trusts) is not on the list. + - title: 2025 Form 1040 instructions, Standard Deduction Worksheet for Dependents + href: https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf#page=35 diff --git a/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py b/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py new file mode 100644 index 00000000000..ddb9c56bba1 --- /dev/null +++ b/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py @@ -0,0 +1,528 @@ +"""Invariants for the filing requirement of a filer who can be claimed. + +`tax_unit_is_required_to_file` gives a filer who can be claimed as a dependent +on another return the three tests of Publication 501 (2025), Table 2 (Chart B +of the Form 1040 instructions; 26 USC 6012(a)(1)(C)(i) and (f), 63(c)(5)). It +codes only the unearned income test. The earned income and gross income tests +come from comparing gross income with `standard_deduction`, which is already +the limited section 63(c)(5) deduction for these filers. Nobody else has an +unearned income test. YAML cases pin the published amounts one case at a time; +these tests check what must hold for every input, which YAML cannot state. + +Where the statute and the IRS charts differ (decision d1395: the age and +blindness addition in the 2018-2025 joint test, a dependent filing +separately, and in 2026 blindness and the exact dollar), the tests below +assert nothing that depends on the difference. + +`test_table_2_boundary_grid` runs one simulation of single filers on a grid of +earned and unearned income at every Table 2 threshold, a dollar under and a +dollar over, for each combination of age 65 and blindness: + +1. Differential: for a filer who can be claimed, the result equals Table 2's + three tests computed directly from its published 2025 amounts. This is the + check that the two tests with no code of their own are still applied. +2. Differential: in 2025 it also equals `medicaid_person_is_required_to_file`, + the other Table 2 implementation, which serves Medicaid and ACA household + income for dependents inside a tax unit. In 2026 the rule is section + 6012(a)(1), whose treatment of blindness and the exact dollar is open, so + the two are not compared there. +3. For a filer nobody can claim, the result depends on gross income alone: it + is the same however that income is split between wages and interest. +4. On a 2025 joint return with no other income, where the spouse who can be + claimed is under 65 and not blind, the result is the unearned income test + (over $1,350), whatever the other spouse's age and blindness. (For a + claimed spouse who is 65 or older or blind, section 6012(f)(2)(D) and + Table 2 differ; that is d1395.) + +`test_filing_requirement_properties` draws batches of tax units with +Hypothesis (single, head of household, surviving spouse, separate and joint; +any mix of age, blindness, claimability, wages, self-employment income or +loss, interest and dividends) and runs each batch, with four variants of every +unit, as one simulation for 2025 and 2026. Every batch starts with two fixed +units, one that must file and one that need not, so that each property below +has rows to check and the batch holds both answers; the test asserts both. + +5. Split invariance: moving a filer's wages into interest, or interest into + wages, never changes the result for a tax unit whose head and spouse cannot + be claimed. +6. Monotonicity: more wages, interest or self-employment profit never turns + "required" into "not required", for any tax unit. +7. Claimability never removes the requirement: marking the head as claimable + never turns "required" into "not required" (filers who are not blind and + do not file separately; see below). +8. A tax unit dependent's wages and interest never change the result: that + income belongs on the dependent's own return. +9. Table 2 and the Medicaid implementation, as in 1 and 2, on the drawn + single filers who can be claimed and have no self-employment income. +10. Lower bound (2025): on a single, separate or joint return, a head or + spouse who can be claimed and whose interest and dividends are over + Table 2's unearned income amount for them always makes the return + required. Table 2's amount is never below the statute's, so this holds + under either reading of d1395. + +`test_earned_income_sources_are_gross_income_sources`: + +11. Every earned income source of the unearned income test is a gross income + source. Otherwise a renamed earned source would silently count as + unearned income. + +Intended exceptions, each from the law rather than the code: + +- Monotonicity does not hold for a smaller self-employment loss. A loss does + not reduce gross income ("don't reduce your income by any losses", Form 1040 + instructions, Chart A) but it does reduce the earned income that sets the + limited standard deduction, so a smaller loss raises the threshold and not + the income. `test_smaller_loss_can_end_the_requirement` pins the case. +- Property 7 leaves out blind filers. The model counts the additional + standard deduction for blindness in every filer's threshold. Chart A does + not (only age), while Table 2 does, so on the charts a blind filer under 65 + with wages between the two amounts must file only if nobody can claim them. + The property holds in the model today, and leaving blind filers out keeps it + true under either reading. It also leaves out separate filers: Table 2 lets + a claimable married dependent filing separately skip a return unless the + spouse itemizes, while sections 6012(f) and (a)(1) require one with any + gross income (decision d1395). +- Properties 1, 2 and 9 use wages as the only earned income. Earned income for + the limited deduction is net of the deductible part of self-employment tax + on the IRS worksheet, which Table 2 does not spell out. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.system import system as SYSTEM + +YEARS = (2025, 2026) + +# Publication 501 (2025), Table 2, single dependents: +# https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=4 +TABLE_2_FLOOR = 1_350 +TABLE_2_EARNED_INCOME_ADDITION = 450 +TABLE_2_EARNED_INCOME_LIMIT = 15_750 +TABLE_2_AGED_OR_BLIND_ADDITION = 2_000 +# The same table, married dependents: $2,950 less $1,350. +TABLE_2_MARRIED_AGED_OR_BLIND_ADDITION = 1_600 + +ADULT_INCOME_INPUTS = [ + "employment_income", + "self_employment_income", + "taxable_interest_income", + "qualified_dividend_income", +] +VARIANTS = ["base", "split", "raised", "claimed", "with_child"] + + +def table_2_single_dependent(earned, unearned, aged, blind): + """Table 2's three tests for a single dependent, from its 2025 amounts.""" + additional = TABLE_2_AGED_OR_BLIND_ADDITION * (aged.astype(int) + blind.astype(int)) + gross = earned + unearned + unearned_test = unearned > TABLE_2_FLOOR + additional + earned_test = earned > TABLE_2_EARNED_INCOME_LIMIT + additional + gross_test = gross > np.maximum( + TABLE_2_FLOOR + additional, + np.minimum(earned, TABLE_2_EARNED_INCOME_LIMIT - TABLE_2_EARNED_INCOME_ADDITION) + + TABLE_2_EARNED_INCOME_ADDITION + + additional, + ) + return unearned_test | earned_test | gross_test + + +def simulate(units): + """One simulation of independent tax units. + + Each unit is {"filing_status": name, "adults": [...], "children": [...]}, + with one dict of person inputs per member. Every unit states its filing + status: an input given to one tax unit is an input for all of them, so a + unit left without one would take the default, SINGLE, not the status the + model would work out. + """ + people, tax_units, households = {}, {}, {} + groups = {"marital_units": {}, "spm_units": {}, "families": {}} + for index, unit in enumerate(units): + adults = [f"adult_{index}_{n}" for n in range(len(unit["adults"]))] + children = [f"child_{index}_{n}" for n in range(len(unit["children"]))] + members = adults + children + for name, inputs in zip(members, unit["adults"] + unit["children"]): + people[name] = { + variable: {year: value for year in YEARS} + for variable, value in inputs.items() + } + tax_units[f"tax_unit_{index}"] = { + "members": members, + "filing_status": {year: unit["filing_status"] for year in YEARS}, + } + households[f"household_{index}"] = { + "members": members, + "state_code": {year: "TX" for year in YEARS}, + } + for name in ("spm_units", "families"): + groups[name][f"{name}_{index}"] = {"members": members} + groups["marital_units"][f"marital_unit_{index}"] = {"members": adults} + for child in children: + groups["marital_units"][f"marital_unit_{child}"] = {"members": [child]} + return Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "households": households, + **groups, + } + ) + + +def single_filer(age, blind, claimed, wages, interest): + return { + "filing_status": "SINGLE", + "adults": [ + { + "age": age, + "is_blind": blind, + "claimed_as_dependent_on_another_return": claimed, + "employment_income": wages, + "taxable_interest_income": interest, + } + ], + "children": [], + } + + +def head_values(units, name): + return np.array([unit["adults"][0].get(name, 0) for unit in units]) + + +def head_medicaid_requirement(simulation, units, year): + """`medicaid_person_is_required_to_file` for each unit's head.""" + values = simulation.calculate("medicaid_person_is_required_to_file", year) + first = np.cumsum([0] + [len(u["adults"]) + len(u["children"]) for u in units]) + return np.asarray(values)[first[:-1]] + + +def unearned_test_met(units): + """Whether Table 2's unearned income test is met by a head or spouse who + can be claimed, for single, separate and joint returns (2025 amounts).""" + met = [] + for unit in units: + married = unit["filing_status"] in ("SEPARATE", "JOINT") + per_condition = ( + TABLE_2_MARRIED_AGED_OR_BLIND_ADDITION + if married + else TABLE_2_AGED_OR_BLIND_ADDITION + ) + met.append( + unit["filing_status"] in ("SINGLE", "SEPARATE", "JOINT") + and any( + adult["claimed_as_dependent_on_another_return"] + and adult["taxable_interest_income"] + + adult.get("qualified_dividend_income", 0) + > TABLE_2_FLOOR + + per_condition * ((adult["age"] >= 65) + adult["is_blind"]) + for adult in unit["adults"] + ) + ) + return np.array(met) + + +def assert_table_2(simulation, units, required, rows, year): + """Properties 1 and 2 (and 9) on the given rows of single claimable filers, + in 2025.""" + assert rows.any(), "no single claimable filer to compare" + if year != 2025: + return + np.testing.assert_array_equal( + required[rows], + head_medicaid_requirement(simulation, units, year)[rows], + err_msg=f"{year}: differs from medicaid_person_is_required_to_file", + ) + expected = table_2_single_dependent( + earned=head_values(units, "employment_income"), + unearned=head_values(units, "taxable_interest_income") + + head_values(units, "qualified_dividend_income"), + aged=head_values(units, "age") >= 65, + blind=head_values(units, "is_blind").astype(bool), + ) + np.testing.assert_array_equal( + required[rows], expected[rows], err_msg="2025: differs from Table 2" + ) + + +# Every Table 2 threshold for a single dependent, a dollar under and over. +EARNED_GRID = [0, 449, 899, 900, 901, 3_000] +EARNED_GRID += [15_299, 15_300, 15_301, 15_749, 15_750, 15_751] +EARNED_GRID += [17_749, 17_750, 17_751, 19_749, 19_750, 19_751] +UNEARNED_GRID = [0, 1, 449, 450, 451, 1_349, 1_350, 1_351] +UNEARNED_GRID += [2_449, 2_450, 2_451, 3_349, 3_350, 3_351] +UNEARNED_GRID += [4_449, 4_450, 4_451, 5_349, 5_350, 5_351] +GROSS_GRID = [600, 1_351, 5_351, 15_749, 15_751, 17_751, 19_751, 21_751, 40_000] + + +def test_table_2_boundary_grid(): + claimable = [ + single_filer(age, blind, True, wages, interest) + for age in (19, 65) + for blind in (False, True) + for wages in EARNED_GRID + for interest in UNEARNED_GRID + ] + # Four ways to hold the same gross income, for a filer nobody can claim. + splits = [ + (age, blind, gross, wages) + for age in (40, 65) + for blind in (False, True) + for gross in GROSS_GRID + for wages in (gross, 0, gross // 2, gross - 600) + ] + unclaimable = [ + single_filer(age, blind, False, wages, gross - wages) + for age, blind, gross, wages in splits + ] + # Joint returns whose younger spouse, under 65 and not blind, can be + # claimed and has only interest. + joint = [ + { + "filing_status": "JOINT", + "adults": [ + { + "age": head_age, + "is_blind": head_blind, + "claimed_as_dependent_on_another_return": False, + "taxable_interest_income": 0, + }, + { + "age": 40, + "is_blind": False, + "claimed_as_dependent_on_another_return": True, + "taxable_interest_income": interest, + }, + ], + "children": [], + } + for head_age in (45, 70, 80) + for head_blind in (False, True) + for interest in UNEARNED_GRID + ] + units = claimable + unclaimable + joint + simulation = simulate(units) + is_claimable = np.arange(len(units)) < len(claimable) + is_joint = np.arange(len(units)) >= len(claimable) + len(unclaimable) + is_unclaimable = ~is_claimable & ~is_joint + for year in YEARS: + required = np.asarray( + simulation.calculate("tax_unit_is_required_to_file", year) + ) + if year == 2025: + # Property 4. + expected = unearned_test_met(units)[is_joint] + np.testing.assert_array_equal(required[is_joint], expected) + assert expected.any() and not expected.all() + # Properties 1 and 2. + assert_table_2(simulation, units, required, is_claimable, year) + # Property 3: one answer for each gross income, whatever the split. + by_split = required[is_unclaimable].reshape(-1, 4) + assert (by_split == by_split[:, :1]).all(), year + # Both answers occur, so the property is not vacuous. + assert by_split.any() and not by_split.all(), year + # The grid reaches both answers of each Table 2 test. + assert required[is_claimable].any() and not required[is_claimable].all() + + +boundary = st.sampled_from(sorted(set(EARNED_GRID + UNEARNED_GRID + [31_499, 31_501]))) +amount = st.one_of(st.just(0), boundary, st.integers(0, 45_000)) +loss = st.integers(-20_000, -1) + + +@st.composite +def adults(draw): + return { + "age": draw(st.sampled_from([19, 40, 64, 65, 80])), + "is_blind": draw(st.booleans()), + "claimed_as_dependent_on_another_return": draw(st.booleans()), + "employment_income": draw(amount), + "self_employment_income": draw(st.one_of(st.just(0), amount, loss)), + "taxable_interest_income": draw(amount), + "qualified_dividend_income": draw(st.one_of(st.just(0), amount)), + } + + +# Fixed units at the start of every batch: a single filer who can be claimed +# with $5,000 of interest (must file; meets every property's row filter) and a +# single filer nobody can claim with $600 of interest (need not file). +ANCHORS = [ + single_filer(19, False, True, 0, 5_000), + single_filer(40, False, False, 0, 600), +] +for anchor in ANCHORS: + anchor["adults"][0].update(self_employment_income=0, qualified_dividend_income=0) + + +@st.composite +def unit_variants(draw, fixed=None): + """A tax unit (drawn, or `fixed`) and its four variants, in VARIANTS order.""" + if fixed is None: + status = draw( + st.sampled_from( + ["SINGLE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE", "SEPARATE", "JOINT"] + ) + ) + base = { + "filing_status": status, + "adults": [draw(adults()) for _ in range(2 if status == "JOINT" else 1)], + "children": [], + } + else: + base = {**fixed, "adults": [dict(adult) for adult in fixed["adults"]]} + + def variant(change): + copy = {**base, "adults": [dict(adult) for adult in base["adults"]]} + change(copy) + return copy + + who = draw(st.integers(0, len(base["adults"]) - 1)) + + # Property 5: move part of one filer's wages into interest, or back. + moved = draw(st.integers(1, 20_000)) + to_interest = draw(st.booleans()) + + def split(unit): + adult = unit["adults"][who] + source, target = "employment_income", "taxable_interest_income" + if not to_interest: + source, target = target, source + shift = min(moved, adult[source]) + adult[source] -= shift + adult[target] += shift + + # Property 6: more of one nonnegative source. A smaller loss is the + # intended exception, so a loss is left alone. + raised_source = draw(st.sampled_from(ADULT_INCOME_INPUTS)) + increase = draw(st.integers(1, 20_000)) + + def raise_income(unit): + adult = unit["adults"][who] + source = raised_source + if adult[source] < 0: + source = "taxable_interest_income" + adult[source] += increase + + # Property 7. + def claim_head(unit): + # The older adult is the head; mark both when the ages tie. + oldest = max(adult["age"] for adult in unit["adults"]) + for adult in unit["adults"]: + if adult["age"] == oldest: + adult["claimed_as_dependent_on_another_return"] = True + + # Property 8. + child = { + "age": draw(st.integers(0, 17)), + "employment_income": draw(amount), + "taxable_interest_income": draw(amount), + } + + def add_child(unit): + unit["children"] = [child] + + return [ + base, + variant(split), + variant(raise_income), + variant(claim_head), + variant(add_child), + ] + + +@settings( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + st.tuples(*(unit_variants(fixed=anchor) for anchor in ANCHORS)), + st.lists(unit_variants(), min_size=6, max_size=22), +) +def test_filing_requirement_properties(anchors, drawn): + units = [unit for variants in [*anchors, *drawn] for unit in variants] + simulation = simulate(units) + anyone_claimable = np.array( + [ + any(a["claimed_as_dependent_on_another_return"] for a in unit["adults"]) + for unit in units + ] + ).reshape(-1, len(VARIANTS)) + separate = np.array( + [unit["filing_status"] == "SEPARATE" for unit in units] + ).reshape(-1, len(VARIANTS)) + anyone_blind = np.array( + [any(adult["is_blind"] for adult in unit["adults"]) for unit in units] + ).reshape(-1, len(VARIANTS)) + # Properties 1, 2 and 9 apply to a single filer who can be claimed and has + # wages as their only earned income. + table_2_rows = np.array( + [ + unit["filing_status"] == "SINGLE" + and unit["adults"][0]["claimed_as_dependent_on_another_return"] + and unit["adults"][0]["self_employment_income"] == 0 + for unit in units + ] + ) + for year in YEARS: + flat = np.asarray(simulation.calculate("tax_unit_is_required_to_file", year)) + required = dict(zip(VARIANTS, flat.reshape(-1, len(VARIANTS)).T)) + nobody_claimable = ~anyone_claimable[:, 0] + # Property 5. + np.testing.assert_array_equal( + required["split"][nobody_claimable], + required["base"][nobody_claimable], + err_msg=f"{year}: the earned/unearned split changed the result", + ) + assert nobody_claimable.any() + # Property 6. + assert not (required["base"] & ~required["raised"]).any(), ( + f"{year}: more income ended the requirement" + ) + assert required["base"].any() and not required["base"].all() + # Property 7. + sighted = ~anyone_blind[:, 0] & ~separate[:, 0] + assert not (required["base"] & ~required["claimed"])[sighted].any(), ( + f"{year}: being claimable ended the requirement" + ) + # Property 8. + np.testing.assert_array_equal( + required["with_child"], + required["base"], + err_msg=f"{year}: a tax unit dependent's income changed the result", + ) + # Property 9. + assert_table_2(simulation, units, flat, table_2_rows, year) + # Property 10. + if year == 2025: + met = unearned_test_met(units) + assert met.any() + assert flat[met].all(), ( + "2025: a claimable filer over the unearned income amount need not file" + ) + + +def test_earned_income_sources_are_gross_income_sources(): + # Property 11. + for year in YEARS: + parameters = SYSTEM.parameters(f"{year}-01-01").gov.irs + earned = list( + parameters.income.filing_requirement.dependent.earned_income_sources + ) + gross = list(parameters.gross_income.sources) + assert earned and set(earned) <= set(gross), set(earned) - set(gross) + + +def test_smaller_loss_can_end_the_requirement(): + # Intended exception to monotonicity. With $2,000 of wages and a $5,000 + # loss, earned income is negative, the limited standard deduction is the + # $1,350 floor, and $2,000 of gross income is over it. With a $100 loss, + # earned income is $1,900 and the limited deduction is $2,350. + units = [single_filer(19, False, True, 2_000, 0) for _ in range(2)] + units[0]["adults"][0]["self_employment_income"] = -5_000 + units[1]["adults"][0]["self_employment_income"] = -100 + required = simulate(units).calculate("tax_unit_is_required_to_file", 2025) + assert required.tolist() == [True, False] diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml new file mode 100644 index 00000000000..17aff4458bf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml @@ -0,0 +1,643 @@ +# Filing requirement for a filer who can be claimed as a dependent on another +# return, and the absence of an unearned income test for everyone else. +# +# 26 U.S.C. 6012(a)(1)(C)(i) and (f), 26 U.S.C. 63(c)(5), and Chart A and +# Chart B of the Form 1040 instructions (Tables 1 and 2 of Publication 501). +# Section 6012(f) governs 2018 to 2025 and section 6012(a)(1) governs 2026. +# For an unmarried filer who can be claimed, section 6012(f)(1) gives Chart +# B's result. Where the statute and the charts differ (decision d1395: the +# age and blindness addition in the 2018-2025 joint test, a dependent filing +# separately, and in 2026 blindness and the exact dollar), the cases stay +# clear of the difference. +# 2025 amounts (Chart B, page 10 of +# https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf): +# Single dependents, under 65 and not blind, must file if +# unearned income was over $1,350, or +# earned income was over $15,750, or +# gross income was more than the larger of $1,350 or earned income (up to +# $15,300) plus $450. +# 65 or older or blind: $3,350, $17,750, and the larger of $3,350 or earned +# income (up to $15,300) plus $2,450. 65 or older and blind: $5,350, +# $19,750, and the larger of $5,350 or earned income plus $4,450. +# Married dependents, 65 or older or blind: unearned income over $2,950 +# (section 6012(f)(2)(D): over $1,350, with no addition). + +# Filers nobody can claim: gross income only. The cases stay a dollar away +# from the threshold itself, where Chart A ("at least") and section 6012(f)(1) +# ("does not exceed the standard deduction") differ; this change does not +# touch that comparison. + +- name: 2025, a single filer nobody can claim with $600 of interest need not file + period: 2025 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 600 + output: + tax_unit_is_required_to_file: false + +- name: 2025, unearned income under the standard deduction does not require a return + period: 2025 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 15_749 + output: + # Chart A: single, under 65, $15,750. + tax_unit_is_required_to_file: false + +- name: 2025, gross income over the standard deduction requires a return + period: 2025 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 15_751 + output: + tax_unit_is_required_to_file: true + +- name: 2025, a single filer aged 70 nobody can claim with $3,000 of interest need not file + period: 2025 + input: + filing_status: SINGLE + age: 70 + taxable_interest_income: 3_000 + output: + # Before, the test was $500 plus the $2,000 additional standard deduction. + tax_unit_is_required_to_file: false + +- name: 2025, a self-employment loss is not unearned income + period: 2025 + input: + filing_status: SINGLE + age: 40 + self_employment_income: -10_000 + output: + # Before, gross income (0) less earned income (-10,000) counted as + # $10,000 of unearned income. + tax_unit_is_required_to_file: false + +- name: 2025, the unearned income test reads the filer, not a child in the tax unit + period: 2025 + input: + people: + parent: + age: 40 + employment_income: 10_000 + claimed_as_dependent_on_another_return: true + child: + age: 10 + taxable_interest_income: 5_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + # The parent has no unearned income, and gross income of $10,000 is not + # more than earned income plus $450. The child's interest belongs on the + # child's own return. + tax_unit_is_required_to_file: false + +# Single dependents, under 65 and not blind. + +- name: 2025, single dependent with unearned income of $1,350 need not file + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_350 + output: + tax_unit_is_required_to_file: false + +- name: 2025, single dependent with unearned income over $1,350 must file + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_351 + output: + tax_unit_is_required_to_file: true + +- name: 2025, single dependent with earned income of $15,750 need not file + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 15_750 + output: + tax_unit_is_required_to_file: false + +- name: 2025, single dependent with earned income over $15,750 must file + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 15_751 + output: + tax_unit_is_required_to_file: true + +- name: 2025, single dependent with gross income equal to earned income plus $450 need not file + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 3_000 + taxable_interest_income: 450 + output: + # $3,450 is not more than $3,000 + $450. + tax_unit_is_required_to_file: false + +- name: 2025, single dependent with gross income over earned income plus $450 must file + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 3_000 + taxable_interest_income: 451 + output: + tax_unit_is_required_to_file: true + +- name: 2025, the earned income in the gross income test stops at $15,300 + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 15_500 + taxable_interest_income: 251 + output: + # $15,751 is more than $15,300 + $450, though not more than + # $15,500 + $450. + tax_unit_is_required_to_file: true + +- name: 2025, the same income does not require a return from a filer nobody can claim + period: 2025 + input: + filing_status: SINGLE + age: 19 + employment_income: 3_000 + taxable_interest_income: 1_351 + output: + tax_unit_is_required_to_file: false + +- name: 2025, a self-employment loss does not add to a single dependent's unearned income + period: 2025 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + self_employment_income: -10_000 + taxable_interest_income: 1_000 + output: + # Unearned income is the $1,000 of interest, not gross income less the + # negative earned income ($11,000). + tax_unit_is_required_to_file: false + +# Single dependents, 65 or older or blind. + +- name: 2025, single dependent aged 65 with unearned income of $3,350 need not file + period: 2025 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_350 + output: + tax_unit_is_required_to_file: false + +- name: 2025, single dependent aged 65 with unearned income over $3,350 must file + period: 2025 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_351 + output: + tax_unit_is_required_to_file: true + +- name: 2025, blind single dependent under 65 with unearned income of $3,350 need not file + period: 2025 + input: + filing_status: SINGLE + age: 30 + is_blind: true + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_350 + output: + tax_unit_is_required_to_file: false + +- name: 2025, blind single dependent under 65 with unearned income over $3,350 must file + period: 2025 + input: + filing_status: SINGLE + age: 30 + is_blind: true + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_351 + output: + tax_unit_is_required_to_file: true + +- name: 2025, blind single dependent aged 65 with unearned income of $5,350 need not file + period: 2025 + input: + filing_status: SINGLE + age: 65 + is_blind: true + claimed_as_dependent_on_another_return: true + taxable_interest_income: 5_350 + output: + tax_unit_is_required_to_file: false + +- name: 2025, blind single dependent aged 65 with unearned income over $5,350 must file + period: 2025 + input: + filing_status: SINGLE + age: 65 + is_blind: true + claimed_as_dependent_on_another_return: true + taxable_interest_income: 5_351 + output: + tax_unit_is_required_to_file: true + +- name: 2025, single dependent aged 65 with earned income of $17,750 need not file + period: 2025 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + employment_income: 17_750 + output: + tax_unit_is_required_to_file: false + +- name: 2025, single dependent aged 65 with earned income over $17,750 must file + period: 2025 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + employment_income: 17_751 + output: + tax_unit_is_required_to_file: true + +- name: 2025, single dependent aged 65 with gross income equal to earned income plus $2,450 need not file + period: 2025 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + employment_income: 3_000 + taxable_interest_income: 2_450 + output: + tax_unit_is_required_to_file: false + +- name: 2025, single dependent aged 65 with gross income over earned income plus $2,450 must file + period: 2025 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + employment_income: 3_000 + taxable_interest_income: 2_451 + output: + tax_unit_is_required_to_file: true + +- name: 2025, blind single dependent aged 65 with earned income of $19,750 need not file + period: 2025 + input: + filing_status: SINGLE + age: 65 + is_blind: true + claimed_as_dependent_on_another_return: true + employment_income: 19_750 + output: + tax_unit_is_required_to_file: false + +- name: 2025, blind single dependent aged 65 with earned income over $19,750 must file + period: 2025 + input: + filing_status: SINGLE + age: 65 + is_blind: true + claimed_as_dependent_on_another_return: true + employment_income: 19_751 + output: + tax_unit_is_required_to_file: true + +# Joint returns: the unearned income test applies to each spouse who can be +# claimed, with that spouse's own income and additional standard deduction. + +- name: 2025, joint return whose spouse can be claimed, unearned income of $1,350 + period: 2025 + input: + people: + head: + age: 40 + spouse: + age: 40 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_350 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + tax_unit_is_required_to_file: false + +- name: 2025, joint return whose spouse can be claimed, unearned income over $1,350 + period: 2025 + input: + people: + head: + age: 40 + spouse: + age: 40 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_351 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + tax_unit_is_required_to_file: true + +- name: 2025, the other spouse's unearned income is not the claimed spouse's + period: 2025 + input: + people: + head: + age: 70 + taxable_interest_income: 1_500 + spouse: + age: 40 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + # The claimed spouse's unearned income is $1,000, not over $1,350. The + # couple's $2,500 would be. Gross income of $2,500 is also under the + # joint standard deduction, whether that is the regular $31,500 plus + # $1,600 or the limited $1,350 plus $1,600 that the Standard Deduction + # Worksheet for Dependents gives when a spouse can be claimed. + tax_unit_is_required_to_file: false + +- name: 2025, joint return whose spouse aged 65 can be claimed, unearned income over $2,950 + # Required under both readings: over $1,350 + $1,600 (Chart B) and over + # $1,350 (section 6012(f)(2)(D)). At $2,950 or less they differ (d1395). + period: 2025 + input: + people: + head: + age: 40 + spouse: + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 2_951 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + tax_unit_is_required_to_file: true + +- name: 2025, a claimed spouse's farm profit is earned income, not unearned income + # Farm income (Schedule 1 line 6) is earned income on the IRS dependent + # worksheet, so the unearned income test does not apply to it. Gross income + # of $2,000 is under the joint standard deduction, whether the regular + # $31,500 plus $1,600 or the limited $1,350 plus $1,600. (Section 6017 + # separately requires a return with $400 or more of net self-employment + # earnings; tax_unit_is_required_to_file covers section 6012 only.) + period: 2025 + input: + people: + head: + age: 70 + spouse: + age: 40 + claimed_as_dependent_on_another_return: true + farm_operations_income: 2_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + tax_unit_is_required_to_file: false + +- name: 2025, the head's age does not raise the claimed spouse's unearned income test + period: 2025 + input: + people: + head: + age: 70 + spouse: + age: 40 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_351 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + tax_unit_is_required_to_file: true + +# 2024 (Chart B: $1,300, $14,600, earned income plus $450; 65 or older or +# blind $3,250): https://www.irs.gov/pub/irs-prior/i1040gi--2024.pdf#page=10 + +- name: 2024, single dependent with unearned income of $1,300 need not file + period: 2024 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_300 + output: + tax_unit_is_required_to_file: false + +- name: 2024, single dependent with unearned income over $1,300 must file + period: 2024 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_301 + output: + tax_unit_is_required_to_file: true + +- name: 2024, single dependent with earned income of $14,600 need not file + period: 2024 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 14_600 + output: + tax_unit_is_required_to_file: false + +- name: 2024, single dependent with earned income over $14,600 must file + period: 2024 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 14_601 + output: + tax_unit_is_required_to_file: true + +- name: 2024, single dependent aged 65 with unearned income of $3,250 need not file + period: 2024 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_250 + output: + tax_unit_is_required_to_file: false + +- name: 2024, single dependent aged 65 with unearned income over $3,250 must file + period: 2024 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_251 + output: + tax_unit_is_required_to_file: true + +- name: 2024, a single filer nobody can claim with $600 of interest need not file + period: 2024 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 600 + output: + tax_unit_is_required_to_file: false + +# 2026 (Rev. Proc. 2025-32, sec. 4.14: $1,350, $450, $16,100 and $2,050): +# https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=18 +# Section 6012(f) ends with 2025, so section 6012(a)(1) applies. Its unearned +# income and gross income tests for these filers, in (a)(1)(C)(i), say "in +# excess of", as Chart B does. But a filer who meets neither is excused only +# by (a)(1)(A)(i), whose gross income must be "less than" the basic standard +# deduction, so on the literal text a return is due at exactly the limited +# deduction. The 2026 instructions are not yet published, so every 2026 case +# stays a dollar away from a threshold. + +- name: 2026, single dependent with unearned income under $1,350 need not file + period: 2026 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_349 + output: + tax_unit_is_required_to_file: false + +- name: 2026, single dependent with unearned income over $1,350 must file + period: 2026 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 1_351 + output: + tax_unit_is_required_to_file: true + +- name: 2026, single dependent with earned income under $16,100 need not file + period: 2026 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 16_099 + output: + tax_unit_is_required_to_file: false + +- name: 2026, single dependent with earned income over $16,100 must file + period: 2026 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 16_101 + output: + tax_unit_is_required_to_file: true + +- name: 2026, single dependent with gross income under earned income plus $450 need not file + period: 2026 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 3_000 + taxable_interest_income: 449 + output: + tax_unit_is_required_to_file: false + +- name: 2026, single dependent with gross income over earned income plus $450 must file + period: 2026 + input: + filing_status: SINGLE + age: 19 + claimed_as_dependent_on_another_return: true + employment_income: 3_000 + taxable_interest_income: 451 + output: + tax_unit_is_required_to_file: true + +- name: 2026, single dependent aged 65 with unearned income under $3,400 need not file + period: 2026 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_399 + output: + # $1,350 + $2,050. + tax_unit_is_required_to_file: false + +- name: 2026, single dependent aged 65 with unearned income over $3,400 must file + period: 2026 + input: + filing_status: SINGLE + age: 65 + claimed_as_dependent_on_another_return: true + taxable_interest_income: 3_401 + output: + tax_unit_is_required_to_file: true + +- name: 2026, a single filer nobody can claim with $600 of interest need not file + period: 2026 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 600 + output: + tax_unit_is_required_to_file: false diff --git a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py index 5c38c142427..512f2c32b26 100644 --- a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py +++ b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py @@ -13,6 +13,12 @@ class tax_unit_is_required_to_file(Variable): definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/6012", + # A filer who can be claimed as a dependent: section 6012(a)(1)(C)(i) + # and (f)(2)(D) (the section above), the section 63(c)(5) limited + # standard deduction, and Publication 501, Table 2 (Chart B of the + # Form 1040 instructions). + "https://www.law.cornell.edu/uscode/text/26/63#c_5", + "https://www.irs.gov/pub/irs-prior/p501--2025.pdf#page=4", # Gross income counts amounts excluded under section 911. "https://www.law.cornell.edu/uscode/text/26/6012#c", ) @@ -39,16 +45,56 @@ def formula(tax_unit, period, parameters): income_over_exemption_amount = gross_income > threshold - # (a)(1)(C) - unearned income threshold for dependents - unearned_income_threshold = 500 + tax_unit( - "additional_standard_deduction", period + # A filer who can be claimed as a dependent on another return (an + # individual described in section 63(c)(5)) must also file when their + # income other than earned income is over the section 63(c)(5)(A) + # amount plus their own additional standard deduction for age or + # blindness: section 6012(a)(1)(C)(i)(I) and Publication 501, Table 2. + # On a joint return the test applies to each spouse who can be claimed. + # Section 6012(f)(2)(D), which governs joint returns for 2018 to 2025, + # names the section 63(c)(5)(A) amount alone; Table 2 adds the + # additional standard deduction, and this follows Table 2 (open as + # decision d1395). Nobody else has an unearned income test. + # + # Table 2's other two tests need no code here. The standard deduction + # in the threshold above is already the limited one for these filers + # (basic_standard_deduction): the greater of the section 63(c)(5)(A) + # amount or earned income plus the section 63(c)(5)(B) amount, capped + # at the regular amount, plus the additional standard deduction. That + # is Table 2's gross income test, and earned income over the regular + # amount plus the additional standard deduction always meets it. + person = tax_unit.members + head_can_be_claimed = tax_unit("head_is_dependent_elsewhere", period) + spouse_can_be_claimed = tax_unit("spouse_is_dependent_elsewhere", period) + can_be_claimed = ( + person("is_tax_unit_head", period) & tax_unit.project(head_can_be_claimed) + ) | ( + person("is_tax_unit_spouse", period) + & tax_unit.project(spouse_can_be_claimed) ) - # The section 911 exclusion covers earned income only. - unearned_income = ( - gross_income - - add(tax_unit, period, ["earned_income"]) - - section_911_exclusion + # Income other than earned income: every gross income source except + # the earned income sources of the IRS dependent worksheet (wages, + # business income and farm income). As in irs_gross_income, a loss in + # one source does not reduce the total. + earned_income_sources = parameters( + period + ).gov.irs.income.filing_requirement.dependent.earned_income_sources + unearned_income = 0 + for source in parameters(period).gov.irs.gross_income.sources: + if source not in earned_income_sources: + unearned_income += max_(0, add(person, period, [source])) + p_standard = parameters(period).gov.irs.deductions.standard + aged_or_blind_count = person("is_irs_aged", period).astype(int) + person( + "is_blind", period + ).astype(int) + additional_standard_deduction = aged_or_blind_count * tax_unit.project( + p_standard.aged_or_blind.amount[tax_unit("filing_status", period)] + ) + unearned_income_threshold = ( + p_standard.dependent.amount + additional_standard_deduction + ) + unearned_income_over_threshold = tax_unit.any( + can_be_claimed & (unearned_income > unearned_income_threshold) ) - unearned_income_over_threshold = unearned_income > unearned_income_threshold return income_over_exemption_amount | unearned_income_over_threshold