Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/claimable-filer-ia-ks-ky-la-md.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply Iowa, Kansas, Louisiana and Maryland rules for a filer who can be claimed as a dependent: no dependent exemptions or credits on a return with a claimable filer outside the claimant filing exception (Iowa dependent credit, Kansas dependent exemption and food sales tax credit child, Louisiana dependent exemption, Maryland dependent and aged-dependent exemptions and child tax credit); a claimed Iowa filer gets the low-income exemption only as a single filer with income below $5,000, and no tax reduction; a claimed Kansas filer has no exemptions in 2021-2023; a Maryland filer who can be claimed gets no personal exemption, poverty line credit or childless EITC, and the poverty line credit stays for a spouse who cannot be claimed. Also limit Maryland's aged dependent exemption by federal AGI under Tax-General 10-211(c), as for the personal exemption.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
description: Iowa exempts a single filer who can be claimed as a dependent on another return from income tax when that filer's income is below this amount.
values:
2021-01-01: 5_000

metadata:
label: Iowa tax exemption income limit for single filers claimed as dependents
period: year
unit: currency-USD
reference:
- title: Iowa Code 422.13(1)(a), filing requirement for a claimed dependent
href: https://www.legis.iowa.gov/docs/code/2026/422.pdf#page=46
- title: 2021 IA1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/2650/download?inline#page=38
- title: 2022 IA1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/2721/download?inline#page=37
- title: 2023 IA 1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/2811/download?inline#page=12
- title: 2024 IA 1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/4152/download?inline#page=10
- title: 2025 IA 1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/4435/download?inline#page=11
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
description: Iowa compares this income of a single filer who can be claimed as a dependent on another return with the dependent tax exemption limit.
values:
# Net income from all sources, IA 1040 line 26.
2021-01-01:
- ia_net_income
# Iowa taxable income, IA 1040 line 4, without adding back deductions.
2023-01-01:
- ia_taxable_income_consolidated

metadata:
unit: list
period: year
label: Iowa tax exemption income sources for single filers claimed as dependents
reference:
- title: 2021 IA1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/2650/download?inline#page=38
- title: 2022 IA1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/2721/download?inline#page=37
- title: 2023 IA 1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/2811/download?inline#page=12
- title: 2024 IA 1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/4152/download?inline#page=10
- title: 2025 IA 1040 Income Tax Return instructions
href: https://revenue.iowa.gov/media/4435/download?inline#page=11
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ class md_total_personal_exemptions(Variable):
defined_for = StateCode.MD

def formula(tax_unit, period, parameters):
# Get md_personal_exemption from tax_unit multiplied by tax_unit_size
# Only the filers' own exemptions remain; a filer who can be
# claimed as a dependent has none (filing status 6).
md_personal_exemption = tax_unit("md_personal_exemption", period)
tax_unit_size = tax_unit("head_spouse_count", period)
return md_personal_exemption * tax_unit_size
own_exemptions = tax_unit(
"head_spouse_count_not_dependent_elsewhere", period
)
return md_personal_exemption * own_exemptions

class mi_personal_exemptions(Variable):
value_type = float
Expand Down Expand Up @@ -351,6 +354,11 @@ def formula(tax_unit, period, parameters):
joint = filing_status == statuses.JOINT
hoh = filing_status == statuses.HEAD_OF_HOUSEHOLD
adults = where(joint | hoh, 2, 1)
# A filer who is claimed as a dependent enters "0" exemptions.
person = tax_unit.members
filer = person("is_tax_unit_head_or_spouse", period)
claimed = person("claimed_as_dependent_on_another_return", period)
adults = where(tax_unit.any(filer & claimed), 0, adults)
return adults * p.consolidated.amount + veterans_exemption_amount

class ma_income_tax_exemption_threshold(Variable):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ def formula(tax_unit, period, parameters):
# dependent is age-eligible, matching the baseline count.
person = tax_unit.members
age = person("age", period)
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)), unless
# the would-be claimant meets the filing exception (Publication 501).
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
if c.age_limit.in_effect:
eligible = is_dependent & (age < c.age_limit.threshold)
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ def formula(tax_unit, period, parameters):

person = tax_unit.members
age = person("age", period)
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)), unless
# the would-be claimant meets the filing exception (Publication 501).
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)

# Apply age limit if in effect.
if p.age_limit.in_effect:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ def formula(tax_unit, period, parameters):
p = parameters(period).gov.contrib.states.md.dependent_exemption
person = tax_unit.members
age = person("age", period)
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)), unless
# the would-be claimant meets the filing exception (Publication 501).
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
if p.age_limit.in_effect:
eligible = is_dependent & (age < p.age_limit.threshold)
else:
Expand Down Expand Up @@ -97,7 +102,12 @@ class md_older_dependents_count(Variable):

def formula(tax_unit, period, parameters):
person = tax_unit.members
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)), unless
# the would-be claimant meets the filing exception (Publication 501).
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
total_dependents = tax_unit.sum(is_dependent)
eligible = tax_unit("md_eligible_dependents_count", period)
return max_(0, total_dependents - eligible)
Expand All @@ -112,12 +122,15 @@ class md_total_personal_exemptions(Variable):

def formula(tax_unit, period, parameters):
# Personal portion keeps the baseline per-person (AGI-stepped)
# amount; dependents are separated out.
# amount; dependents are separated out. These are the filers' own
# exemptions (none for a filer who can be claimed as a dependent)
# plus over-age dependents.
per_person = tax_unit("md_personal_exemption", period)
tax_unit_size = tax_unit("tax_unit_size", period)
dependents = tax_unit("tax_unit_dependents", period)
own_exemptions = tax_unit(
"head_spouse_count_not_dependent_elsewhere", period
)
older_dependents = tax_unit("md_older_dependents_count", period)
personal_count = tax_unit_size - dependents + older_dependents
personal_count = own_exemptions + older_dependents
personal_amount = personal_count * per_person

dependent_exemption_amount = tax_unit("md_dependent_exemption", period)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
"""Iowa, Kansas, Kentucky, Louisiana and Maryland rules for a filer who can be
claimed as a dependent.

A return on which the filer (or, if joint, either spouse) can be claimed has no
dependents unless the would-be claimant meets the filing exception (IRC
152(b)(1), Publication 501), so these states' dependent exemptions and credits,
Kansas's food sales tax credit child route, Maryland's child tax credit and the
childless pro forma EITC follow that test. Iowa's low-income exemption and tax
reduction and Kansas's 2021-2023 exemptions turn on an actual claim; Maryland's
own exemption and poverty line credit turn on claimability (filing status 6).

For couples drawn by Hypothesis (claimable, claimed and filing-exception
patterns) and a seeded population in these states:

1. Swap invariance: each output is the same under either head/spouse
labelling.
2. Monotonicity: marking another filer as claimed never raises the amounts in
MONOTONE. This holds for the drawn inputs, which have no self-employment
losses: on separate Maryland returns a claimable spouse's loss no longer
offsets the other spouse's earnings, which is intended.
3. Identities: with a filer claimable outside the filing exception there is no
Louisiana dependent exemption, Maryland aged dependent exemption or child
tax credit, and no pro forma federal EITC without a child; with every filer
claimable, no poverty line credit.
4. Differential: wherever the federal EITC's eligibility holds, Maryland's
pro forma federal EITC without the minimum age equals the federal credit
before take-up and filing.
5. Unchanged when nobody can be claimed: Maryland's exemption count is the tax
unit's size, the poverty line credit's earned income and poverty level are
`eitc_earned_income` and `tax_unit_fpg`, and Louisiana's dependent
exemption is $1,000 per dependent.
6. Filing exception scope: the exception never lowers the 152(b)(1) amounts in
EXCEPTION_RESTORES and does not change the claimability and actual-claim
rules in EXCEPTION_NEUTRAL.
"""

import numpy as np
import pytest
from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st

from policyengine_us import Simulation
from policyengine_us.tests.core.test_claimable_filer_federal_invariants import (
check_monotone,
check_swap,
)
from policyengine_us.tests.core.test_dependent_elsewhere_head_spouse_swap_invariance import (
CLAIM_PATTERNS,
TOLERANCE,
_calc,
_crafted_cases,
_exception_variants,
_seeded_couples,
_situation,
adults,
dependents,
)

STATES = ["IA", "KS", "KY", "LA", "MD"]
# 2022: Iowa's net-income path, Kansas's exemption count and Louisiana's
# exemptions; 2024: Iowa's consolidated path and Kansas's filing-status
# allowance; 2026: Louisiana's flat tax and Maryland's child tax credit
# phase-out.
YEARS = [2022, 2024, 2026]
MONOTONE = [
"ia_exemption_credit",
"ks_exemptions",
"ks_fstc",
"la_dependents_exemption",
"md_personal_exemptions_count",
"md_total_personal_exemptions",
"md_aged_dependent_exemption",
"federal_eitc_without_age_minimum",
"md_ctc",
"md_poverty_line_credit_earned_income",
]
OUTPUTS = MONOTONE + [
"ia_is_tax_exempt",
"ia_income_tax",
"ks_income_tax",
"ky_income_tax",
"la_income_tax",
"is_eligible_md_poverty_line_credit",
"md_poverty_line_credit",
"md_local_poverty_line_credit",
"md_income_tax",
"state_income_tax",
]
# 152(b)(1) amounts that the claimant filing exception restores.
EXCEPTION_RESTORES = [
"ia_exemption_credit",
"ks_exemptions",
"ks_fstc",
"la_dependents_exemption",
"md_personal_exemptions_count",
"md_aged_dependent_exemption",
"federal_eitc_without_age_minimum",
"md_ctc",
]
# Rules on claimability or an actual claim, which the exception leaves alone.
EXCEPTION_NEUTRAL = [
"ia_is_tax_exempt",
"md_poverty_line_credit_earned_income",
]


@st.composite
def couples(draw):
claimable = draw(st.sampled_from(CLAIM_PATTERNS))
claimed = tuple(can_claim and draw(st.booleans()) for can_claim in claimable)
return {
"state": draw(st.sampled_from(STATES)),
"adults": [draw(adults()), draw(adults())],
"dependents": draw(st.lists(dependents(), max_size=2)),
"claimed": claimed,
"claimable": claimable,
"exception": tuple(
not is_claimed and draw(st.booleans()) for is_claimed in claimed
),
}


def _identities(sim, year):
claimable = _calc(sim, "head_or_spouse_is_dependent_elsewhere", year) > 0
no_dependents = (
_calc(
sim, "head_or_spouse_is_dependent_elsewhere_without_filing_exception", year
)
> 0
)
every = _calc(sim, "every_filer_is_dependent_elsewhere", year) > 0
childless = _calc(sim, "eitc_child_count", year) == 0
for name in ["la_dependents_exemption", "md_aged_dependent_exemption", "md_ctc"]:
assert not _calc(sim, name, year)[no_dependents].any(), name
pro_forma = _calc(sim, "federal_eitc_without_age_minimum", year)
assert not pro_forma[no_dependents & childless].any()
assert not _calc(sim, "is_eligible_md_poverty_line_credit", year)[every].any()
# Each tax unit has its own household, in the same order.
state = np.asarray(sim.calculate("state_code_str", year))
md, la = state == "MD", state == "LA"
# 5. Unchanged when nobody can be claimed.
nobody = ~claimable
np.testing.assert_array_equal(
_calc(sim, "md_personal_exemptions_count", year)[md & nobody],
_calc(sim, "tax_unit_size", year)[md & nobody],
)
np.testing.assert_allclose(
_calc(sim, "md_poverty_line_credit_earned_income", year)[md & nobody],
_calc(sim, "eitc_earned_income", year)[md & nobody],
atol=TOLERANCE,
)
np.testing.assert_allclose(
_calc(sim, "md_poverty_line_credit_income_level", year)[md & nobody],
_calc(sim, "tax_unit_fpg", year)[md & nobody],
atol=TOLERANCE,
)
np.testing.assert_allclose(
_calc(sim, "la_dependents_exemption", year)[la & nobody],
1_000 * _calc(sim, "tax_unit_dependents", year)[la & nobody],
atol=TOLERANCE,
)
# 4. Differential: the federal credit before take-up and filing.
federal_eligible = md & (_calc(sim, "eitc_eligible", year) > 0)
federal = np.minimum(
_calc(sim, "eitc_phased_in", year),
np.maximum(
0, _calc(sim, "eitc_maximum", year) - _calc(sim, "eitc_reduction", year)
),
)
np.testing.assert_allclose(
pro_forma[federal_eligible], federal[federal_eligible], atol=TOLERANCE
)


@settings(
max_examples=3,
deadline=None,
derandomize=True,
suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
)
@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=12))
def test_ia_ks_ky_la_md_claimable_filer_rules_are_label_free(year, units):
check_swap(units, year, OUTPUTS, _identities)


@pytest.mark.parametrize("year", YEARS)
def test_ia_ks_ky_la_md_claimable_filer_rules_seeded_population(year):
units = _seeded_couples(STATES, per_state=3) + _crafted_cases(STATES)
check_swap(units, year, OUTPUTS, _identities)
check_monotone(units, year, MONOTONE, _identities)


@pytest.mark.parametrize("year", [2022, 2025])
def test_ia_ks_ky_la_md_filing_exception_scope(year):
# Each unit runs four times with nobody actually claimed: claimable
# without the exception (both labellings), then with it.
units = [
unit
for unit in _seeded_couples(STATES, per_state=3) + _crafted_cases(STATES)
if any(unit["claimed"])
]
sim = Simulation(situation=_situation(units, year, _exception_variants))
for name in EXCEPTION_RESTORES + EXCEPTION_NEUTRAL:
copies = _calc(sim, name, year).reshape(-1, 4)
np.testing.assert_allclose(
copies[:, 0::2], copies[:, 1::2], atol=TOLERANCE, rtol=0
)
without, with_exception = copies[:, :2], copies[:, 2:]
if name in EXCEPTION_NEUTRAL:
np.testing.assert_allclose(
with_exception,
without,
atol=TOLERANCE,
rtol=0,
err_msg=f"The filing exception changes {name}",
)
else:
assert not (with_exception < without - TOLERANCE).any(), (
f"The filing exception lowers {name}"
)
Loading