Skip to content

Add Ohio homestead property tax exemption#8310

Open
daphnehanse11 wants to merge 10 commits into
PolicyEngine:mainfrom
daphnehanse11:codex/oh-homestead-property-tax-exemption
Open

Add Ohio homestead property tax exemption#8310
daphnehanse11 wants to merge 10 commits into
PolicyEngine:mainfrom
daphnehanse11:codex/oh-homestead-property-tax-exemption

Conversation

@daphnehanse11

@daphnehanse11 daphnehanse11 commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds Ohio's Homestead Exemption for senior, disabled, and qualifying surviving-spouse homeowners.
  • Adds 2025 and 2026 indexed exemption amounts and the income limit parameter.
  • Models the exempt assessed value and the corresponding property-tax reduction.
  • Keeps the rule household-calculator-only for now; this PR does not wire the reduction into baseline property-tax-credit aggregates while assessed-property-value data readiness is unresolved.

Closes #8200.

Tests

  • PYTHONPATH=. /Users/daphnehansell/Documents/GitHub/policyengine-us/.venv/bin/python -m policyengine_core.scripts.policyengine_command test policyengine_us/tests/policy/baseline/gov/states/oh/tax/property/homestead_exemption -c policyengine_us
  • /Users/daphnehansell/Documents/GitHub/policyengine-us/.venv/bin/ruff check policyengine_us/variables/gov/states/oh/tax/property/homestead_exemption

@codecov

codecov Bot commented May 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (7857608) to head (91d9afb).
⚠️ Report is 127 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #8310   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            2         5    +3     
  Lines           36        71   +35     
=========================================
+ Hits            36        71   +35     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@daphnehanse11 daphnehanse11 marked this pull request as draft May 14, 2026 20:29
@daphnehanse11 daphnehanse11 requested a review from hua7450 June 15, 2026 20:47
@daphnehanse11 daphnehanse11 marked this pull request as ready for review June 15, 2026 20:47
@hua7450

hua7450 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Program Review: Ohio Homestead Exemption (PR #8310)

Source Documents

  • ORC §323.151 (HTML) — "total income" = modified adjusted gross income (per §5747.01) of owner + spouse for the year preceding application. https://codes.ohio.gov/ohio-revised-code/section-323.151
  • ORC §323.152 (HTML) — exemption = $25,000 true-value base "as adjusted" (indexed annually each September by GDP deflator); income limit = $30,000 base "as adjusted," rounded to nearest $100; assessment % "not to exceed thirty-five per cent"; age 65 / permanently-and-totally-disabled / surviving spouse 59–64 at deceased spouse's death. https://codes.ohio.gov/ohio-revised-code/section-323.152
  • ORC §5715.01 (HTML) — assessment percentage set by tax commissioner, not to exceed 35% of true value.
  • HB 261 LSC Fiscal Note (key=27544, dated Mar 20 2026) — current-law baseline as of TY2026: income limit "currently $41,000"; exempted market value "$29,700 in TY 2026, indexed annually." Bill PROPOSES raising limit to $55,000 (not current law). https://www.legislature.ohio.gov/download?key=27544 (current-law values on p.2)
  • HB 103 LSC Fiscal Note (key=25178, dated May 6 2025) — current-law baseline for TY2025: income limit raised "from $40,000 to $45,000 for TY 2025" (current law = $40,000); exempted market value "from $29,000 to $50,000 for TY 2025" (current law = $29,000). $45,000/$50,000 are PROPOSED. https://www.legislature.ohio.gov/download?key=25178 (current-law values on p.2). NOTE: the source manifest mislabels this as "HB 261" — it is HB 103.
  • Geauga County Auditor — "Homestead Exemption 2026 Requirements" brochure — income limit $41,000 (2025 MAGI for a 2026 application); savings "calculated on $29,000 of taxable value"; age 65 / disabled; enhanced $58,000 disabled-veteran exemption. https://auditor.geauga.oh.gov/wp-content/uploads/sites/2/2026/01/homestead-exemption-2026.pdf (values on p.1–2)
  • External county-auditor / state sources (cross-verification): Butler, Lake, Stark, Franklin County auditors; Ohio Senate FAQ; Ohio Legal Help.

Branch Status

⚠ PR branch is 8 commit(s) behind main. Consider rebasing before merging. Review was scoped to the PR's actual changes — staleness did not affect findings.


Critical (Must Fix)

  1. income_limit.yaml TY2025 value is wrong — should be $40,000, not $41,000.
    The parameter has a single entry 2025-01-01: 41_000, so both income_limit(2025) and income_limit(2026) resolve to $41,000. The correct current-law income threshold (modified Ohio AGI of owner + spouse) is TY2025 = $40,000 and TY2026 = $41,000. The repo's $41,000 is the TY2026 value applied a year early; TY2025 is overstated by $1,000.

    • Confirmed by code-path verification: oh_homestead_exemption_eligible.py:38 reads p.income_limit at the bare simulation period (not period.last_year), so for a TY2025 calculation it returns $41,000. The prior-year offset lives only on the income side (oh_homestead_exemption_total_income reads oh_modified_agi at period.last_year) and does NOT shift the benefit-year-keyed threshold. The sibling amount.yaml (also read at bare period) is correctly split 29_000@2025 / 29_700@2026, which is the tell that income_limit must likewise be benefit-year-keyed. A wrong threshold directly flips eligibility — and zeroes/enables both oh_homestead_exemption and oh_homestead_property_tax_reduction (both defined_for = "oh_homestead_exemption_eligible") — for the income band $40,000 < 2024 MAGI ≤ $41,000 in TY2025.
    • Confirmed by 600 DPI visual audit: HB 103 fiscal note p.2 "raises the income threshold for eligibility from $40,000 to $45,000 for TY 2025" (current law = $40,000); HB 261 fiscal note p.2 "currently $41,000" (TY2026); Geauga 2026 brochure p.1–2 gates a 2026 application (Jan 1 2026 residence = TY2026) on $41,000 of 2025 income.
    • Confirmed by external sources: 5 county auditors (Butler, Lake, Stark, Franklin + Ohio Senate FAQ) uniformly report TY2025 = $40,000 / TY2026 = $41,000 (High confidence).
    • Fix: Split into two entries, mirroring amount.yaml:
      2025-01-01: 40_000   # TY2025 current law (HB 103 FN, "from $40,000 ... for TY 2025")
      2026-01-01: 41_000   # TY2026 current law (HB 261 FN "currently $41,000"; Geauga 2026 brochure)
      Add a period: 2025 eligibility test at the $40,000 boundary once fixed (the existing eligibility tests are all period: 2026 at the $41,000 boundary, so they pass today and hide the TY2025 error).
  2. oh_homestead_exemption_total_income formula has ZERO unit-test coverage — its formula is never executed (codecov root cause).
    The variable has a real formula (return tax_unit("oh_modified_agi", period.last_year)), but all three test files set oh_homestead_exemption_total_income as a direct INPUT (30_000 / 41_000 / 41_001), which short-circuits the formula. The period.last_year prior-year offset — the entire point of the variable — is never run. This matches the failing codecov/patch + codecov/project checks (the only failing CI; all functional checks pass).

    • Worse, eligible Case 8 is named "prior-year total income controls eligibility" and sets oh_modified_agi: 0 AND oh_homestead_exemption_total_income: 41_001 — but because the latter is a direct input, the formula never reads oh_modified_agi, so the test does not actually verify the prior-year behavior its name claims.
    • Fix: Add a test that does NOT set oh_homestead_exemption_total_income; instead, in period 2026 set oh_modified_agi for 2025 (prior year) only and assert oh_homestead_exemption_total_income as an OUTPUT equal to that prior-year value, plus a companion case proving current-year oh_modified_agi is ignored. This covers the formula and makes Case 8's claim real.

Should Address

  1. amount.yaml TY2026 = $29,700 is CONTESTED — verify before merge. (If confirmed wrong, this becomes CRITICAL.)

    • TY2025 = $29,000 is confirmed correct by all sources (HB 103 FN p.2 "from $29,000 ... for TY 2025"; Geauga brochure "$29,000 of taxable value"; Butler/Franklin auditors; Ohio Legal Help).
    • TY2026 = $29,700 is corroborated only by the repo's own cited source — the HB 261 fiscal note p.2: "the exempted market value ... is estimated to be $29,700 in TY 2026, with that amount indexed annually for inflation." However, every accessible real-world 2026 county-auditor guide (Butler, Franklin) and Ohio Legal Help show $29,000 flat for TY2026, not $29,700 — and those same sources DID update the income limit to $41,000 for 2026, so the flat $29,000 exemption is not merely a stale page. The external verifier rates this "Medium-High likely wrong; cannot positively confirm either value."
    • Candidate values: $29,700 (HB 261 fiscal-note estimate, repo's value) vs. $29,000 (multiple 2026 county guides). Recommend confirming against the official Ohio Dept. of Taxation homestead indexing bulletin / DTE 105A guidance (the authoritative per-TY indexed-amount source) before merge. If the team confirms $29,000, change 2026-01-01: 29_70029_000 and reclassify this as CRITICAL.
  2. Weak citations on the two dollar-value parameters (amount, income_limit).
    Both cite ORC §323.152 + the HB 261 fiscal note. ORC §323.152 contains only the base, un-indexed figures ($25,000 true value "as adjusted"; $30,000 income "as adjusted") — NOT the indexed $29,000/$29,700/$40,000/$41,000 the parameters store. The HB 261 fiscal note is a proposed-bill artifact whose headline numbers ($55,000 / $50,000) contradict the parameter values, and the indexed figures appear only in its incidental "under current law" sentences. Recommend: keep §323.152 as the statutory authority, and replace the proposed-bill fiscal note with the Ohio Dept. of Taxation annual homestead indexing bulletin / DTE 105A guidance (the document that actually publishes the per-TY indexed dollar amounts). This source could not be retrieved automatically this session (tax.ohio.gov homestead pages returned 404) — flag for manual retrieval per the unreachable-reference checkpoint. In the interim the Geauga County brochure is a cleaner current-law citation than a proposed-bill note (secondary, for cross-verification only).

  3. Add subsection IDs to parameter reference titles. All five params cite the bare section ("Ohio Revised Code Section 323.152"). Recommended specific subsections (verified against the live statute): amount → §323.152(A)(1)(c)(i); income_limit → §323.152(A)(1)(b)(iii); assessment_rate → §323.152(A)(1)(c)(ii) + §5715.01(B); age_threshold → §323.152(A)(1)(a)(ii); surviving_spouse_age_threshold → §323.152(A)(1)(a)(iii).

  4. Deprecated documentation field on a Variable. oh_homestead_exemption_total_income.py:9 uses documentation = "...". The documentation class field on Variables is deprecated — move the prose into an inline # comment inside the formula and keep reference = (already present). None of the other four variables use it.

  5. Missing dollar-output boundary tests.

    • No oh_homestead_exemption / oh_homestead_property_tax_reduction test at income 41,001 expecting 0 (the defined_for zeroing path on the dollar outputs — currently both only test income 30,000).
    • No married-couple-filing-jointly case anywhere: every multi-person test uses filing_status: SINGLE with the 2nd person as a non-joint adult, so the tax_unit_is_joint branch of head_or_spouse (counting a spouse's assessed value/taxes) is never exercised as true for eligibility, exemption, or reduction.
    • No surviving-spouse just-below-threshold case (age 58 + qualifying_surviving_spouse: true → ineligible) to lock the >= 59 operator.
    • No 2025-period case on oh_homestead_property_tax_reduction (its tests only hit the 2026 indexed amount).
    • (Minor) The assessed-value cap is exercised only on the aged-single path, not the disabled-under-65 or surviving-spouse path.
  6. Division-guard boundary semantics in oh_homestead_property_tax_reduction.py:27-29. real_estate_taxes * (exemption / max_(assessed_value, 1)) avoids divide-by-zero, but at assessed_value == 0 the floor-at-1 makes the ratio exemption / 1 rather than 0. It is safe today only because oh_homestead_exemption = min_(assessed_value, …) is also 0 when assessed value is 0 — the safety depends on that coupling, not the guard. Consider where(assessed_value > 0, real_estate_taxes * exemption / assessed_value, 0) to make the zero case explicitly 0; at minimum, confirm the coupling is intended.

  7. Head/spouse property attribution is triplicated across three files (oh_homestead_exemption.py, _eligible.py, _property_tax_reduction.py) as head | (is_tax_unit_spouse & tax_unit_is_joint) rather than reusing the existing is_tax_unit_head_or_spouse helper (which KY uses). The OH version additionally gates the spouse's property on joint filing — this appears intentional (count a spouse's home value only when filing jointly) and is exercised by tests, but has no regulatory citation. Consider extracting a shared oh_homestead_exemption_qualifying_owner Person-level variable to remove the triplication and add a comment justifying the joint-filing condition.

Suggestions

  1. Surviving-spouse path is a documented modeling limitation, not a bug. oh_homestead_exemption_qualifying_surviving_spouse.py is a bare Person-level boolean input (no formula, defaults False), so the age-59 branch (§323.152(A)(1)(a)(iii)) only fires via explicit test input (eligible Case 5) and is effectively never triggered in microsimulation. This is acceptable: PolicyEngine has no "date the deceased spouse died" / "age at spouse's death" input, so a fully-correct formula is not feasible. The authors intentionally kept it input-only — eligible Case 7 deliberately asserts that federal SURVIVING_SPOUSE filing status alone is NOT sufficient. Suggestion: add a docstring/inline comment documenting that this pathway is not populated in microdata (a known limitation), rather than leaving a silent always-false input. (Do not add default_value=False — framework default.)

  2. Document the real_estate_taxes > 0 ownership/occupancy proxy. The exemption requires owning + occupying a principal residence on January 1; PolicyEngine has no ownership/principal-residence input, so the formula proxies it with real_estate_taxes > 0. This is reasonable but over-inclusive (a second home or rental with property tax would pass) and cannot enforce the statutory ≤1-acre homesite limit (no parcel-acreage input). Document both as known limitations in the variable.

  3. Surviving-spouse upper age bound (65) not modeled. §323.152(A)(1)(a)(iii) says "at least fifty-nine but not sixty-five or more"; the formula checks only age >= 59. A surviving spouse aged 65+ qualifies via the age-65 path anyway, so the result is correct — a one-line comment noting the harmless omission would help.

  4. oh_homestead_property_tax_reduction is orphaned (not wired into net income or property tax). No downstream variable consumes it — it does not feed OH income tax, a property-tax-credit aggregator, household_tax, household_benefits, or programs.yaml. This matches the established codebase pattern (KY/SC/MS homestead reductions are likewise standalone, because PolicyEngine does not net property-tax reductions into household net income). Per the PR body this is explicitly household-calculator-only (it does NOT wire into baseline aggregates), so the population-bias impact is low priority. If standalone is intended, state so in the PR description; if it should lower OH property-tax liability, wire it into the relevant variable.

  5. Out-of-scope enhanced categories — note as deliberate exclusions in the PR description. Not modeled: (a) the enhanced disabled-veteran exemption ($58,000 market value, 100% disability rating); (b) the surviving spouse of a public-service officer killed in the line of duty; (c) the pre-2014 (TY2013) grandfathered recipients who are exempt from the income test. All three are confirmed in the fiscal notes / Geauga brochure and are reasonable simplifications (PE does not track 100%-VA-rating, line-of-duty death, or 2013 application vintage). The repo correctly does NOT use the PROPOSED HB 261 / HB 103 figures ($55,000 / $50,000 / $45,000).

  6. Manifest mislabel (housekeeping, not a code issue). The source manifest lists key=25178 as "HB 261" — it is actually HB 103. The parameter files do not cite key=25178, so no live citation is wrong, but correct the manifest so a future maintainer does not add the wrong-document citation.

  7. Uniform period access in oh_homestead_exemption_eligible.py. Line 20 reads age at period.this_year while the rest of the formula uses bare period. Because the formula is YEAR-defined, period.this_year == period so there is no live numeric defect — but the mix is inconsistent and would silently break if the variable were ever switched to MONTH. Use bare period for age to match the sibling files.

PDF Audit Summary

Category Count
Confirmed correct 4 (assessment_rate 0.35; age 65; surviving-spouse age 59; amount TY2025 $29,000)
Mismatches confirmed 1 (income_limit TY2025 $41,000 → should be $40,000)
Mismatches contested 1 (amount TY2026 $29,700 — cited source says $29,700, county guides say $29,000; verify)
Unmodeled items (by design) 3 ($58,000 disabled-veteran exemption; line-of-duty officer surviving spouse; pre-2014 grandfathered no-income-test)

Validation Summary

Check Result
Regulatory Core mechanics correct (true value × 35% capped; effective-rate reduction; prior-year MAGI; age/disability gating). 1 value error (income_limit TY2025) + 1 documented limitation (surviving-spouse input).
Reference Quality All values traceable to some source, but the two dollar params cite a permanent statute (base figures only) + a proposed-bill fiscal note. Recommend Dept. of Taxation indexing bulletin. Subsection IDs missing from titles.
Code Patterns Clean — no hard-coded values, correct vectorization/aggregation, no eternity, changelog present. Minor: deprecated documentation field; division-guard boundary; triplicated head/spouse logic; mixed period access.
Test Coverage Eligibility well covered (10 cases). 1 formula variable (oh_homestead_exemption_total_income) has ZERO formula coverage; missing joint-couple, income-above-limit dollar outputs, surviving-spouse just-below, 2025 reduction.
PDF Value Audit 4 confirmed / 1 confirmed mismatch / 1 contested / 3 unmodeled (by design).
CI Status codecov/patch + codecov/project FAIL (zero coverage on oh_homestead_exemption_total_income, and oh_homestead_exemption_qualifying_surviving_spouse has no formula). All functional checks pass.

Review Severity: REQUEST_CHANGES

Two CRITICAL findings (income_limit TY2025 value mismatch; zero-coverage formula causing the codecov failure) require fixes before merge. The contested amount TY2026 = $29,700 should be verified against the Ohio Dept. of Taxation indexing bulletin and may escalate to CRITICAL if the county guides' $29,000 is confirmed.

Next Steps

To auto-fix issues: /fix-pr 8310

@hua7450 hua7450 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — see the detailed review comment above. Two blockers before merge:

  1. income_limit.yaml TY2025 = $41,000 is wrong — should be $40,000 ($41,000 is the TY2026 value, applied a year early). Confirmed via code-path + 600 DPI + 5 county auditors. Fix: split 40_000@2025 / 41_000@2026, mirroring amount.yaml.
  2. oh_homestead_exemption_total_income formula has zero test coverage (set as a direct input everywhere) — the root cause of the failing codecov checks. Add a test that sets prior-year oh_modified_agi and asserts the variable as an output.

Also please verify amount.yaml TY2026 = $29,700 against the Ohio Dept. of Taxation indexing bulletin — the cited fiscal note says $29,700 but 2026 county guides show $29,000; this escalates to a blocker if $29,000 is confirmed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Ohio homestead exemption for older homeowners

2 participants