Trigger
File when all confirmed 3.9 users (currently: Census) have migrated to 3.10+.
Background
Supporting 3.9 across the stack required working around non-trivial syntax and dependency cruft in four repos. ~95% of the cruft is 3.9-specific — dropping 3.9 alone recovers most of it, even if 3.10 support remains (3.10 hits EOL Oct 2026, ~6 months out, so keeping it through natural life is cheap).
Landed as: PolicyEngine/policyengine-core#454, PolicyEngine/policyengine-us#8035, PolicyEngine/policyengine-uk#1625, #278.
Checklist
All 4 repos
policyengine-core (PolicyEngine/policyengine-core)
policyengine-us (PolicyEngine/policyengine-us)
policyengine-uk (PolicyEngine/policyengine-uk)
policyengine.py (this repo)
Not in scope
Keep until 3.10 EOL (October 2026):
class Foo(str, Enum) pattern (4 files: outputs/{aggregate,change_aggregate,inequality,poverty}.py) — StrEnum is 3.11+
timezone.utc in core/tax_benefit_model_version.py — datetime.UTC is 3.11+
- Gotcha when
StrEnum returns: str(Foo.X) differs — StrEnum returns "x", (str, Enum) returns "Foo.X". Audit any code relying on str() of enum members before reverting.
Keep until 3.12 EOL (October 2028):
TypeVar + Generic[T] pattern in core/cache.py and core/output.py — PEP 695 class Foo[T] is 3.12+.
Verification
After cleanup, each repo's CI should be green with reduced matrix (3.10+). Cross-repo smoke: pip install policyengine[us] and pip install policyengine[uk] succeed on 3.10 in a fresh venv.
Trigger
File when all confirmed 3.9 users (currently: Census) have migrated to 3.10+.
Background
Supporting 3.9 across the stack required working around non-trivial syntax and dependency cruft in four repos. ~95% of the cruft is 3.9-specific — dropping 3.9 alone recovers most of it, even if 3.10 support remains (3.10 hits EOL Oct 2026, ~6 months out, so keeping it through natural life is cheap).
Landed as: PolicyEngine/policyengine-core#454, PolicyEngine/policyengine-us#8035, PolicyEngine/policyengine-uk#1625, #278.
Checklist
All 4 repos
pyproject.toml: raiserequires-pythonfrom>=3.9to>=3.10Programming Language :: Python :: 3.9classifier"3.9"from all CI Python matricespolicyengine-core (PolicyEngine/policyengine-core)
numpy>=2.1.0,<3(2.1+ supports 3.10+)Optional[X]/Tuple[X]→X | None/tuple[X]inpolicyengine_core/tools/hugging_face.pyandpolicyengine_core/tools/google_cloud.py(~5 sites; this repo's ruff config doesn't have UP rules enabled)policyengine-us (PolicyEngine/policyengine-us)
[tool.uv] environmentsblock inpyproject.tomlif it still exists (likely already gone once downstream core picks up 3.9-compat release)Optional[X]→X | Nonein:policyengine_us/system.py,data/economic_assumptions.py,data/dataset_schema.py,tests/core/test_payroll_contributions.py,tests/microsimulation/data/fixtures/test_extend_single_year_dataset.py(5 sites)uv.lockpolicyengine-uk (PolicyEngine/policyengine-uk)
[tool.uv] environmentsblock (if still present)policyengine_uk/data/dataset_schema.pyandpolicyengine_uk/simulation.pypolicyengine_uk/tests/test_build_metadata.py: revertExitStackback to parenthesizedwith (a, b, c):[tool.ruff] target-version = "py39"→"py310"uv.lockpolicyengine.py (this repo)
pyproject.toml:[tool.ruff] target-version = "py39"→"py310"[tool.mypy] python_version = "3.9"→"3.10"UP006,UP007,UP035,UP045from[tool.ruff.lint] ignoreruff check --fix --unsafe-fixes .— this re-applies UP045 (Optional[X]→X | None), UP007 (Union[X, Y]→X | Y), UP006/UP035 (List→list).src/policyengine/core/scoping_strategy.py:222(insideAnnotated[]) andsrc/policyengine/core/region.py:18(module-level type alias).Python-CompatCI job in.github/workflows/pr_code_changes.yaml— drop the explicith5pyinstall (needed becausescoping_strategy.pyimports it but extras weren't available; once[us]/[uk]extras are bumped to versions that pull h5py via core, it's redundant).[us]/[uk]/[dev]extras offpolicyengine-us==1.602.0/policyengine-uk==2.74.0to versions that don't pin 3.11+ themselves (separate follow-up from this cleanup — may already be bumped by the time this issue is actioned).Not in scope
Keep until 3.10 EOL (October 2026):
class Foo(str, Enum)pattern (4 files:outputs/{aggregate,change_aggregate,inequality,poverty}.py) — StrEnum is 3.11+timezone.utcincore/tax_benefit_model_version.py—datetime.UTCis 3.11+StrEnumreturns:str(Foo.X)differs —StrEnumreturns"x",(str, Enum)returns"Foo.X". Audit any code relying onstr()of enum members before reverting.Keep until 3.12 EOL (October 2028):
TypeVar+Generic[T]pattern incore/cache.pyandcore/output.py— PEP 695class Foo[T]is 3.12+.Verification
After cleanup, each repo's CI should be green with reduced matrix (3.10+). Cross-repo smoke:
pip install policyengine[us]andpip install policyengine[uk]succeed on 3.10 in a fresh venv.