From 2c6e78d9ed67206005fe65fe53a7396a3f83000c Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 4 May 2026 19:56:12 +0200 Subject: [PATCH 1/5] Reject monthly US household inputs --- .../tax_benefit_models/us/household.py | 51 +++++++++++++++++-- tests/test_household_impact.py | 34 +++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index 5258043a..2fd8567a 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -92,6 +92,48 @@ def _safe_convert(value: Any) -> Any: return str(value) if value is not None else None +def _validate_annual_year(year: Any) -> int: + if isinstance(year, bool): + raise ValueError( + "US household calculations require a calendar year as an integer, " + "for example year=2026. Monthly periods are not supported by " + "pe.us.calculate_household." + ) + if isinstance(year, int): + return year + if isinstance(year, str) and year.isdecimal() and len(year) == 4: + return int(year) + raise ValueError( + "US household calculations require a calendar year as an integer, " + "for example year=2026. Monthly periods are not supported by " + "pe.us.calculate_household." + ) + + +def _validate_unperiodized_household_inputs( + *, + people: list[Mapping[str, Any]], + entities: Mapping[str, Mapping[str, Any]], +) -> None: + for index, person in enumerate(people): + for variable, value in person.items(): + if variable != "id" and isinstance(value, Mapping): + raise ValueError( + "Periodized US household inputs are not supported by " + "pe.us.calculate_household. Pass annual scalar input values " + f"only; received a periodized value for people[{index}].{variable}." + ) + + for entity, values in entities.items(): + for variable, value in values.items(): + if variable != "id" and isinstance(value, Mapping): + raise ValueError( + "Periodized US household inputs are not supported by " + "pe.us.calculate_household. Pass annual scalar input values " + f"only; received a periodized value for {entity}.{variable}." + ) + + def _build_situation( *, people: list[Mapping[str, Any]], @@ -181,13 +223,13 @@ def calculate_household( if a variable is placed on the wrong entity (e.g. ``filing_status`` on ``people``), or if ``extra_variables`` / ``reform`` names a variable or parameter path not defined - on the US model. + on the US model. Raises if ``year`` is not an annual calendar + year or if household input values are already periodized. """ if unexpected: _raise_unexpected_kwargs(unexpected) - from policyengine_us import Simulation - + year = _validate_annual_year(year) people = list(people) entities = { "marital_unit": dict(marital_unit or {}), @@ -196,6 +238,9 @@ def calculate_household( "tax_unit": dict(tax_unit or {}), "household": dict(household or {}), } + _validate_unperiodized_household_inputs(people=people, entities=entities) + + from policyengine_us import Simulation validate_household_input( model_version=us_latest, diff --git a/tests/test_household_impact.py b/tests/test_household_impact.py index d99d144b..73a42b84 100644 --- a/tests/test_household_impact.py +++ b/tests/test_household_impact.py @@ -119,6 +119,40 @@ def test__reform_compiles_effective_date_form(self): ) assert result.tax_unit.ctc >= 0 + def test__monthly_year_period__then_raises_before_calculation(self): + with pytest.raises(ValueError, match="Monthly periods are not supported"): + pe.us.calculate_household( + people=[{"age": 30, "is_tax_unit_head": True}], + year="2026-01", + ) + + def test__periodized_person_input__then_raises_before_calculation(self): + with pytest.raises( + ValueError, + match=r"Periodized US household inputs.*people\[0\]\.employment_income", + ): + pe.us.calculate_household( + people=[ + { + "age": 30, + "is_tax_unit_head": True, + "employment_income": {"2026-01": 1_000}, + } + ], + year=2026, + ) + + def test__periodized_group_input__then_raises_before_calculation(self): + with pytest.raises( + ValueError, + match=r"Periodized US household inputs.*household\.state_code", + ): + pe.us.calculate_household( + people=[{"age": 30, "is_tax_unit_head": True}], + household={"state_code": {"2026-01": "CA"}}, + year=2026, + ) + class TestHouseholdInputValidation: def test__unknown_person_variable__then_raises_with_suggestion(self): From 7c88de901a4ab255732580dbc033e44ce44a6d89 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 4 May 2026 20:40:37 +0200 Subject: [PATCH 2/5] Share annual household input validation --- .../tax_benefit_models/common/__init__.py | 3 + .../tax_benefit_models/common/household.py | 59 +++++++++++++++++++ .../tax_benefit_models/uk/household.py | 13 +++- .../tax_benefit_models/us/household.py | 52 +++------------- tests/test_household_impact.py | 32 +++++++++- 5 files changed, 111 insertions(+), 48 deletions(-) create mode 100644 src/policyengine/tax_benefit_models/common/household.py diff --git a/src/policyengine/tax_benefit_models/common/__init__.py b/src/policyengine/tax_benefit_models/common/__init__.py index 654f350d..744bf21d 100644 --- a/src/policyengine/tax_benefit_models/common/__init__.py +++ b/src/policyengine/tax_benefit_models/common/__init__.py @@ -6,6 +6,9 @@ """ from .extra_variables import dispatch_extra_variables as dispatch_extra_variables +from .household import ( + validate_annual_household_inputs as validate_annual_household_inputs, +) from .model_version import ( MicrosimulationModelVersion as MicrosimulationModelVersion, ) diff --git a/src/policyengine/tax_benefit_models/common/household.py b/src/policyengine/tax_benefit_models/common/household.py new file mode 100644 index 00000000..ab169325 --- /dev/null +++ b/src/policyengine/tax_benefit_models/common/household.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any + + +def validate_annual_household_inputs( + *, + year: Any, + entities: Mapping[str, Sequence[Mapping[str, Any]]], +) -> int: + """Validate annual-only household calculator inputs.""" + validated_year = _validate_annual_year(year) + _validate_unperiodized_values(entities) + return validated_year + + +def _validate_annual_year(year: Any) -> int: + if isinstance(year, bool): + raise _annual_period_error() + if isinstance(year, int): + return year + if isinstance(year, str) and year.isdecimal() and len(year) == 4: + return int(year) + raise _annual_period_error() + + +def _annual_period_error() -> ValueError: + return ValueError( + "Household calculations require a calendar year as an integer, " + "for example year=2026. Monthly periods are not supported by " + "calculate_household." + ) + + +def _validate_unperiodized_values( + entities: Mapping[str, Sequence[Mapping[str, Any]]], +) -> None: + for entity, records in entities.items(): + for index, record in enumerate(records): + for variable, value in record.items(): + if variable != "id" and isinstance(value, Mapping): + raise ValueError( + "Periodized household inputs are not supported by " + "calculate_household. Pass annual scalar input values " + f"only; received a periodized value for " + f"{_input_location(entity, index, len(records), variable)}." + ) + + +def _input_location( + entity: str, + index: int, + record_count: int, + variable: str, +) -> str: + if record_count == 1 and entity != "people": + return f"{entity}.{variable}" + return f"{entity}[{index}].{variable}" diff --git a/src/policyengine/tax_benefit_models/uk/household.py b/src/policyengine/tax_benefit_models/uk/household.py index 5dbd71bb..9d4fb163 100644 --- a/src/policyengine/tax_benefit_models/uk/household.py +++ b/src/policyengine/tax_benefit_models/uk/household.py @@ -28,6 +28,7 @@ HouseholdResult, compile_reform, dispatch_extra_variables, + validate_annual_household_inputs, ) from policyengine.utils.household_validation import validate_household_input @@ -139,11 +140,19 @@ def calculate_household( if unexpected: _raise_unexpected_kwargs(unexpected) - from policyengine_uk import Simulation - people = list(people) benunit_dict = dict(benunit or {}) household_dict = dict(household or {}) + year = validate_annual_household_inputs( + year=year, + entities={ + "people": people, + "benunit": [benunit_dict], + "household": [household_dict], + }, + ) + + from policyengine_uk import Simulation validate_household_input( model_version=uk_latest, diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index 2fd8567a..76902f00 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -45,6 +45,7 @@ HouseholdResult, compile_reform, dispatch_extra_variables, + validate_annual_household_inputs, ) from policyengine.utils.household_validation import validate_household_input @@ -92,48 +93,6 @@ def _safe_convert(value: Any) -> Any: return str(value) if value is not None else None -def _validate_annual_year(year: Any) -> int: - if isinstance(year, bool): - raise ValueError( - "US household calculations require a calendar year as an integer, " - "for example year=2026. Monthly periods are not supported by " - "pe.us.calculate_household." - ) - if isinstance(year, int): - return year - if isinstance(year, str) and year.isdecimal() and len(year) == 4: - return int(year) - raise ValueError( - "US household calculations require a calendar year as an integer, " - "for example year=2026. Monthly periods are not supported by " - "pe.us.calculate_household." - ) - - -def _validate_unperiodized_household_inputs( - *, - people: list[Mapping[str, Any]], - entities: Mapping[str, Mapping[str, Any]], -) -> None: - for index, person in enumerate(people): - for variable, value in person.items(): - if variable != "id" and isinstance(value, Mapping): - raise ValueError( - "Periodized US household inputs are not supported by " - "pe.us.calculate_household. Pass annual scalar input values " - f"only; received a periodized value for people[{index}].{variable}." - ) - - for entity, values in entities.items(): - for variable, value in values.items(): - if variable != "id" and isinstance(value, Mapping): - raise ValueError( - "Periodized US household inputs are not supported by " - "pe.us.calculate_household. Pass annual scalar input values " - f"only; received a periodized value for {entity}.{variable}." - ) - - def _build_situation( *, people: list[Mapping[str, Any]], @@ -229,7 +188,6 @@ def calculate_household( if unexpected: _raise_unexpected_kwargs(unexpected) - year = _validate_annual_year(year) people = list(people) entities = { "marital_unit": dict(marital_unit or {}), @@ -238,7 +196,13 @@ def calculate_household( "tax_unit": dict(tax_unit or {}), "household": dict(household or {}), } - _validate_unperiodized_household_inputs(people=people, entities=entities) + year = validate_annual_household_inputs( + year=year, + entities={ + "people": people, + **{name: [value] for name, value in entities.items()}, + }, + ) from policyengine_us import Simulation diff --git a/tests/test_household_impact.py b/tests/test_household_impact.py index 73a42b84..a6214239 100644 --- a/tests/test_household_impact.py +++ b/tests/test_household_impact.py @@ -65,6 +65,34 @@ def test__reform_changes_child_benefit__then_dict_compiles_and_applies(self): assert isinstance(reformed.benunit.child_benefit, float) assert isinstance(baseline.benunit.child_benefit, float) + def test__monthly_year_period__then_raises_before_calculation(self): + with pytest.raises(ValueError, match="Monthly periods are not supported"): + pe.uk.calculate_household( + people=[{"age": 30}], + year="2026-01", + ) + + def test__periodized_person_input__then_raises_before_calculation(self): + with pytest.raises( + ValueError, + match=r"Periodized household inputs.*people\[0\]\.employment_income", + ): + pe.uk.calculate_household( + people=[{"age": 30, "employment_income": {"2026-01": 1_000}}], + year=2026, + ) + + def test__periodized_group_input__then_raises_before_calculation(self): + with pytest.raises( + ValueError, + match=r"Periodized household inputs.*benunit\.would_claim_child_benefit", + ): + pe.uk.calculate_household( + people=[{"age": 30}], + benunit={"would_claim_child_benefit": {"2026-01": True}}, + year=2026, + ) + class TestUSCalculateHousehold: def test__single_adult__then_returns_result_with_net_income(self): @@ -129,7 +157,7 @@ def test__monthly_year_period__then_raises_before_calculation(self): def test__periodized_person_input__then_raises_before_calculation(self): with pytest.raises( ValueError, - match=r"Periodized US household inputs.*people\[0\]\.employment_income", + match=r"Periodized household inputs.*people\[0\]\.employment_income", ): pe.us.calculate_household( people=[ @@ -145,7 +173,7 @@ def test__periodized_person_input__then_raises_before_calculation(self): def test__periodized_group_input__then_raises_before_calculation(self): with pytest.raises( ValueError, - match=r"Periodized US household inputs.*household\.state_code", + match=r"Periodized household inputs.*household\.state_code", ): pe.us.calculate_household( people=[{"age": 30, "is_tax_unit_head": True}], From 9ffb7cfc7e428883f1bcbde10abfa5985717a3ae Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Tue, 5 May 2026 21:52:34 +0200 Subject: [PATCH 3/5] Address household input review comments --- .../tax_benefit_models/common/household.py | 23 +++++++++++-------- .../tax_benefit_models/uk/household.py | 11 +++++---- .../tax_benefit_models/us/household.py | 6 ++--- tests/test_household_impact.py | 19 ++++++++++++++- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/policyengine/tax_benefit_models/common/household.py b/src/policyengine/tax_benefit_models/common/household.py index ab169325..e926b062 100644 --- a/src/policyengine/tax_benefit_models/common/household.py +++ b/src/policyengine/tax_benefit_models/common/household.py @@ -1,35 +1,38 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import Any +from typing import Any, Union + +AnnualYear = Union[int, str] def validate_annual_household_inputs( *, year: Any, entities: Mapping[str, Sequence[Mapping[str, Any]]], -) -> int: +) -> AnnualYear: """Validate annual-only household calculator inputs.""" validated_year = _validate_annual_year(year) _validate_unperiodized_values(entities) return validated_year -def _validate_annual_year(year: Any) -> int: +def _validate_annual_year(year: Any) -> AnnualYear: if isinstance(year, bool): - raise _annual_period_error() + raise _annual_period_error(year) if isinstance(year, int): return year if isinstance(year, str) and year.isdecimal() and len(year) == 4: - return int(year) - raise _annual_period_error() + return year + raise _annual_period_error(year) -def _annual_period_error() -> ValueError: +def _annual_period_error(year: Any) -> ValueError: return ValueError( - "Household calculations require a calendar year as an integer, " - "for example year=2026. Monthly periods are not supported by " - "calculate_household." + "Household calculations require a calendar year as an integer " + "or four-digit string, for example year=2026 or year='2026'. " + "Monthly periods are not supported by calculate_household. " + f"Received year={year!r}." ) diff --git a/src/policyengine/tax_benefit_models/uk/household.py b/src/policyengine/tax_benefit_models/uk/household.py index 9d4fb163..2390867d 100644 --- a/src/policyengine/tax_benefit_models/uk/household.py +++ b/src/policyengine/tax_benefit_models/uk/household.py @@ -21,7 +21,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, Optional +from typing import Any, Optional, Union from policyengine.tax_benefit_models.common import ( EntityResult, @@ -62,7 +62,7 @@ def _build_situation( people: list[Mapping[str, Any]], benunit: Mapping[str, Any], household: Mapping[str, Any], - year: int, + year: Union[int, str], ) -> dict[str, Any]: year_str = str(year) @@ -110,7 +110,7 @@ def calculate_household( people: list[Mapping[str, Any]], benunit: Optional[Mapping[str, Any]] = None, household: Optional[Mapping[str, Any]] = None, - year: int = 2026, + year: Union[int, str] = 2026, reform: Optional[Mapping[str, Any]] = None, extra_variables: Optional[list[str]] = None, **unexpected: Any, @@ -132,8 +132,9 @@ def calculate_household( :class:`HouseholdResult` with dot-accessible entity results. Raises: - ValueError: on unknown or mis-placed variable names, or - unknown reform parameter paths. + ValueError: on unknown or mis-placed variable names, + unknown reform parameter paths, non-annual ``year`` values, + or periodized household input values. TypeError: on US-only kwargs (``tax_unit``, etc.) or other unsupported keyword arguments. """ diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index 76902f00..9a571618 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -38,7 +38,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, Optional +from typing import Any, Optional, Union from policyengine.tax_benefit_models.common import ( EntityResult, @@ -101,7 +101,7 @@ def _build_situation( spm_unit: Mapping[str, Any], tax_unit: Mapping[str, Any], household: Mapping[str, Any], - year: int, + year: Union[int, str], ) -> dict[str, Any]: year_str = str(year) @@ -147,7 +147,7 @@ def calculate_household( spm_unit: Optional[Mapping[str, Any]] = None, tax_unit: Optional[Mapping[str, Any]] = None, household: Optional[Mapping[str, Any]] = None, - year: int = 2026, + year: Union[int, str] = 2026, reform: Optional[Mapping[str, Any]] = None, extra_variables: Optional[list[str]] = None, **unexpected: Any, diff --git a/tests/test_household_impact.py b/tests/test_household_impact.py index a6214239..d1de6e23 100644 --- a/tests/test_household_impact.py +++ b/tests/test_household_impact.py @@ -9,7 +9,11 @@ import pytest import policyengine as pe -from policyengine.tax_benefit_models.common import EntityResult, HouseholdResult +from policyengine.tax_benefit_models.common import ( + EntityResult, + HouseholdResult, + validate_annual_household_inputs, +) class TestUKCalculateHousehold: @@ -183,6 +187,19 @@ def test__periodized_group_input__then_raises_before_calculation(self): class TestHouseholdInputValidation: + def test__annual_year_string__then_preserves_string_year(self): + assert ( + validate_annual_household_inputs(year="2026", entities={"people": []}) + == "2026" + ) + + def test__non_annual_year__then_error_includes_received_year(self): + with pytest.raises(ValueError, match=r"Received year='2026-01'"): + validate_annual_household_inputs( + year="2026-01", + entities={"people": []}, + ) + def test__unknown_person_variable__then_raises_with_suggestion(self): with pytest.raises(ValueError, match="employment_incme"): pe.us.calculate_household( From 9a111dc75b03399ff634483c98e8df6760ef1ce1 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Tue, 5 May 2026 21:55:39 +0200 Subject: [PATCH 4/5] Allow string years in reform compilation types --- src/policyengine/tax_benefit_models/common/reform.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/policyengine/tax_benefit_models/common/reform.py b/src/policyengine/tax_benefit_models/common/reform.py index 60b564f4..5280893b 100644 --- a/src/policyengine/tax_benefit_models/common/reform.py +++ b/src/policyengine/tax_benefit_models/common/reform.py @@ -40,7 +40,7 @@ import datetime from collections.abc import Mapping from difflib import get_close_matches -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, Union if TYPE_CHECKING: from policyengine.core.dynamic import Dynamic @@ -51,7 +51,7 @@ def compile_reform( reform: Optional[Mapping[str, Any]], *, - year: Optional[int] = None, + year: Optional[Union[int, str]] = None, model_version: Optional[TaxBenefitModelVersion] = None, ) -> Optional[dict[str, dict[str, Any]]]: """Compile a simple reform dict to the core reform-dict format. @@ -97,7 +97,7 @@ def compile_reform( def _reform_dict_to_parameter_values( reform: Mapping[str, Any], *, - year: Optional[int], + year: Optional[Union[int, str]], model_version: TaxBenefitModelVersion, ) -> list: """Compile a flat reform dict into a list of ``ParameterValue`` objects. @@ -138,7 +138,7 @@ def _compile_reform_to( default_name: str, reform: Optional[Mapping[str, Any]], *, - year: Optional[int], + year: Optional[Union[int, str]], model_version: TaxBenefitModelVersion, name: Optional[str] = None, ): @@ -153,7 +153,7 @@ def _compile_reform_to( def compile_reform_to_policy( reform: Optional[Mapping[str, Any]], *, - year: Optional[int], + year: Optional[Union[int, str]], model_version: TaxBenefitModelVersion, name: Optional[str] = None, ) -> Optional[Policy]: @@ -180,7 +180,7 @@ def compile_reform_to_policy( def compile_reform_to_dynamic( reform: Optional[Mapping[str, Any]], *, - year: Optional[int], + year: Optional[Union[int, str]], model_version: TaxBenefitModelVersion, name: Optional[str] = None, ) -> Optional[Dynamic]: From 8d71e4eca69c3e0ecf314a3bd982d7ccdf679363 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Wed, 6 May 2026 21:14:12 +0200 Subject: [PATCH 5/5] Normalize household years to int --- .../tax_benefit_models/common/household.py | 14 ++++++-------- .../tax_benefit_models/common/reform.py | 12 ++++++------ .../tax_benefit_models/uk/household.py | 6 +++--- .../tax_benefit_models/us/household.py | 6 +++--- tests/test_household_impact.py | 4 ++-- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/policyengine/tax_benefit_models/common/household.py b/src/policyengine/tax_benefit_models/common/household.py index e926b062..e3aced33 100644 --- a/src/policyengine/tax_benefit_models/common/household.py +++ b/src/policyengine/tax_benefit_models/common/household.py @@ -1,36 +1,34 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import Any, Union - -AnnualYear = Union[int, str] +from typing import Any def validate_annual_household_inputs( *, year: Any, entities: Mapping[str, Sequence[Mapping[str, Any]]], -) -> AnnualYear: +) -> int: """Validate annual-only household calculator inputs.""" validated_year = _validate_annual_year(year) _validate_unperiodized_values(entities) return validated_year -def _validate_annual_year(year: Any) -> AnnualYear: +def _validate_annual_year(year: Any) -> int: if isinstance(year, bool): raise _annual_period_error(year) if isinstance(year, int): return year if isinstance(year, str) and year.isdecimal() and len(year) == 4: - return year + return int(year) raise _annual_period_error(year) def _annual_period_error(year: Any) -> ValueError: return ValueError( - "Household calculations require a calendar year as an integer " - "or four-digit string, for example year=2026 or year='2026'. " + "Household calculations require a calendar year as an integer, " + "for example year=2026. " "Monthly periods are not supported by calculate_household. " f"Received year={year!r}." ) diff --git a/src/policyengine/tax_benefit_models/common/reform.py b/src/policyengine/tax_benefit_models/common/reform.py index 5280893b..60b564f4 100644 --- a/src/policyengine/tax_benefit_models/common/reform.py +++ b/src/policyengine/tax_benefit_models/common/reform.py @@ -40,7 +40,7 @@ import datetime from collections.abc import Mapping from difflib import get_close_matches -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional if TYPE_CHECKING: from policyengine.core.dynamic import Dynamic @@ -51,7 +51,7 @@ def compile_reform( reform: Optional[Mapping[str, Any]], *, - year: Optional[Union[int, str]] = None, + year: Optional[int] = None, model_version: Optional[TaxBenefitModelVersion] = None, ) -> Optional[dict[str, dict[str, Any]]]: """Compile a simple reform dict to the core reform-dict format. @@ -97,7 +97,7 @@ def compile_reform( def _reform_dict_to_parameter_values( reform: Mapping[str, Any], *, - year: Optional[Union[int, str]], + year: Optional[int], model_version: TaxBenefitModelVersion, ) -> list: """Compile a flat reform dict into a list of ``ParameterValue`` objects. @@ -138,7 +138,7 @@ def _compile_reform_to( default_name: str, reform: Optional[Mapping[str, Any]], *, - year: Optional[Union[int, str]], + year: Optional[int], model_version: TaxBenefitModelVersion, name: Optional[str] = None, ): @@ -153,7 +153,7 @@ def _compile_reform_to( def compile_reform_to_policy( reform: Optional[Mapping[str, Any]], *, - year: Optional[Union[int, str]], + year: Optional[int], model_version: TaxBenefitModelVersion, name: Optional[str] = None, ) -> Optional[Policy]: @@ -180,7 +180,7 @@ def compile_reform_to_policy( def compile_reform_to_dynamic( reform: Optional[Mapping[str, Any]], *, - year: Optional[Union[int, str]], + year: Optional[int], model_version: TaxBenefitModelVersion, name: Optional[str] = None, ) -> Optional[Dynamic]: diff --git a/src/policyengine/tax_benefit_models/uk/household.py b/src/policyengine/tax_benefit_models/uk/household.py index 2390867d..53c386d1 100644 --- a/src/policyengine/tax_benefit_models/uk/household.py +++ b/src/policyengine/tax_benefit_models/uk/household.py @@ -21,7 +21,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any, Optional from policyengine.tax_benefit_models.common import ( EntityResult, @@ -62,7 +62,7 @@ def _build_situation( people: list[Mapping[str, Any]], benunit: Mapping[str, Any], household: Mapping[str, Any], - year: Union[int, str], + year: int, ) -> dict[str, Any]: year_str = str(year) @@ -110,7 +110,7 @@ def calculate_household( people: list[Mapping[str, Any]], benunit: Optional[Mapping[str, Any]] = None, household: Optional[Mapping[str, Any]] = None, - year: Union[int, str] = 2026, + year: int = 2026, reform: Optional[Mapping[str, Any]] = None, extra_variables: Optional[list[str]] = None, **unexpected: Any, diff --git a/src/policyengine/tax_benefit_models/us/household.py b/src/policyengine/tax_benefit_models/us/household.py index 9a571618..76902f00 100644 --- a/src/policyengine/tax_benefit_models/us/household.py +++ b/src/policyengine/tax_benefit_models/us/household.py @@ -38,7 +38,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any, Optional from policyengine.tax_benefit_models.common import ( EntityResult, @@ -101,7 +101,7 @@ def _build_situation( spm_unit: Mapping[str, Any], tax_unit: Mapping[str, Any], household: Mapping[str, Any], - year: Union[int, str], + year: int, ) -> dict[str, Any]: year_str = str(year) @@ -147,7 +147,7 @@ def calculate_household( spm_unit: Optional[Mapping[str, Any]] = None, tax_unit: Optional[Mapping[str, Any]] = None, household: Optional[Mapping[str, Any]] = None, - year: Union[int, str] = 2026, + year: int = 2026, reform: Optional[Mapping[str, Any]] = None, extra_variables: Optional[list[str]] = None, **unexpected: Any, diff --git a/tests/test_household_impact.py b/tests/test_household_impact.py index d1de6e23..88444ebc 100644 --- a/tests/test_household_impact.py +++ b/tests/test_household_impact.py @@ -187,10 +187,10 @@ def test__periodized_group_input__then_raises_before_calculation(self): class TestHouseholdInputValidation: - def test__annual_year_string__then_preserves_string_year(self): + def test__annual_year_string__then_normalizes_to_int(self): assert ( validate_annual_household_inputs(year="2026", entities={"people": []}) - == "2026" + == 2026 ) def test__non_annual_year__then_error_includes_received_year(self):