Skip to content

Commit cf3c940

Browse files
MaxGhenisclaude
andauthored
Read retirement limits from policyengine-us parameters (#566)
* Read retirement limits from policyengine-us parameters Replace hard-coded RETIREMENT_LIMITS dict with a shared utility that reads from policyengine-us's parameter tree at runtime. This ensures limits stay in sync as policyengine-us is updated, and eliminates a maintenance risk when the same dict is duplicated in puf_impute.py (PR #554). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixup! Read retirement limits from policyengine-us parameters * Revert test formatting to match CI Black version The fixup reformatted SQL strings in test_database_build.py using a newer Black version that disagrees with CI's lgeiger/black-action. Revert to the original formatting that CI accepts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add tests for retirement limits utility Verifies get_retirement_limits returns correct IRS contribution limits for 2020, 2023, and 2025 by comparing against known values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Handle both old and new policyengine-us catch-up parameter layouts The SECURE 2.0 update (merged to pe-us main, not yet released) renames children["401k"] to children["k401"] and changes it from a simple int to an age-bracketed scale. Handle both layouts so the code works with the current release and future releases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use k401 parameter directly, bump policyengine-us>=1.572.5 Drop dual-path handling in favor of the current parameter layout (k401 scale with age brackets from SECURE 2.0). Bump the minimum policyengine-us version to 1.572.5 which introduced this parameter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 090cfa1 commit cf3c940

6 files changed

Lines changed: 83 additions & 49 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Read IRS retirement contribution limits from policyengine-us parameters instead of hard-coding them.

policyengine_us_data/datasets/cps/cps.py

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -649,51 +649,11 @@ def add_personal_income_variables(
649649
# Disregard reported pension contributions from people who report neither wage and salary
650650
# nor self-employment income.
651651
# Assume no 403(b) or 457 contributions for now.
652-
# IRS retirement contribution limits by year.
653-
RETIREMENT_LIMITS = {
654-
2020: {
655-
"401k": 19_500,
656-
"401k_catch_up": 6_500,
657-
"ira": 6_000,
658-
"ira_catch_up": 1_000,
659-
},
660-
2021: {
661-
"401k": 19_500,
662-
"401k_catch_up": 6_500,
663-
"ira": 6_000,
664-
"ira_catch_up": 1_000,
665-
},
666-
2022: {
667-
"401k": 20_500,
668-
"401k_catch_up": 6_500,
669-
"ira": 6_000,
670-
"ira_catch_up": 1_000,
671-
},
672-
2023: {
673-
"401k": 22_500,
674-
"401k_catch_up": 7_500,
675-
"ira": 6_500,
676-
"ira_catch_up": 1_000,
677-
},
678-
2024: {
679-
"401k": 23_000,
680-
"401k_catch_up": 7_500,
681-
"ira": 7_000,
682-
"ira_catch_up": 1_000,
683-
},
684-
2025: {
685-
"401k": 23_500,
686-
"401k_catch_up": 7_500,
687-
"ira": 7_000,
688-
"ira_catch_up": 1_000,
689-
},
690-
}
691-
# Clamp to the nearest available year for out-of-range values.
692-
clamped_year = max(
693-
min(year, max(RETIREMENT_LIMITS)),
694-
min(RETIREMENT_LIMITS),
652+
from policyengine_us_data.utils.retirement_limits import (
653+
get_retirement_limits,
695654
)
696-
limits = RETIREMENT_LIMITS[clamped_year]
655+
656+
limits = get_retirement_limits(year)
697657
LIMIT_401K = limits["401k"]
698658
LIMIT_401K_CATCH_UP = limits["401k_catch_up"]
699659
LIMIT_IRA = limits["ira"]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Tests for retirement contribution limits utility."""
2+
3+
import pytest
4+
from policyengine_us_data.utils.retirement_limits import (
5+
get_retirement_limits,
6+
)
7+
8+
# Expected values sourced from IRS announcements and policyengine-us
9+
# parameter tree.
10+
EXPECTED = {
11+
2020: {
12+
"401k": 19_500,
13+
"401k_catch_up": 6_500,
14+
"ira": 6_000,
15+
"ira_catch_up": 1_000,
16+
},
17+
2023: {
18+
"401k": 22_500,
19+
"401k_catch_up": 7_500,
20+
"ira": 6_500,
21+
"ira_catch_up": 1_000,
22+
},
23+
2025: {
24+
"401k": 23_500,
25+
"401k_catch_up": 7_500,
26+
"ira": 7_000,
27+
"ira_catch_up": 1_000,
28+
},
29+
}
30+
31+
32+
@pytest.mark.parametrize("year", EXPECTED.keys())
33+
def test_retirement_limits(year):
34+
limits = get_retirement_limits(year)
35+
assert limits == EXPECTED[year]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Retirement contribution limits from policyengine-us parameters.
2+
3+
Reads IRS contribution limits from the policyengine-us parameter tree
4+
instead of hard-coding them.
5+
"""
6+
7+
from functools import lru_cache
8+
9+
10+
@lru_cache(maxsize=16)
11+
def get_retirement_limits(year: int) -> dict:
12+
"""Return contribution limits for the given tax year.
13+
14+
Reads from policyengine-us parameters at:
15+
gov.irs.gross_income.retirement_contributions.limit.{401k, ira}
16+
gov.irs.gross_income.retirement_contributions.catch_up.limit.{k401, ira}
17+
18+
The k401 catch-up parameter is a SingleAmountTaxScale with age
19+
brackets (SECURE 2.0); we use the age-50 bracket for the standard
20+
catch-up amount.
21+
22+
Returns:
23+
Dict with keys: 401k, 401k_catch_up, ira, ira_catch_up.
24+
"""
25+
from policyengine_us import CountryTaxBenefitSystem
26+
27+
tbs = CountryTaxBenefitSystem()
28+
p = tbs.parameters.gov.irs.gross_income.retirement_contributions
29+
d = f"{year}-01-01"
30+
31+
return {
32+
"401k": int(p.limit.children["401k"](d)),
33+
"401k_catch_up": int(p.catch_up.limit.children["k401"](d).calc(50)),
34+
"ira": int(p.limit.ira(d)),
35+
"ira_catch_up": int(p.catch_up.limit.ira(d)),
36+
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ classifiers = [
2121
"Programming Language :: Python :: 3.13",
2222
]
2323
dependencies = [
24-
"policyengine-us>=1.516.0",
24+
"policyengine-us>=1.572.5",
2525
"policyengine-core>=3.23.6",
2626
"pandas>=2.3.1",
2727
"requests>=2.25.0",

uv.lock

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)