Skip to content

Fix FunctionInputOutputTypeChecker crash on list[int] | None annotations - #1736

Merged
skrawcz merged 1 commit into
apache:mainfrom
breken-ai:fix/check-instance-pep604-union
Sep 26, 2026
Merged

skrawcz merged 1 commit into
apache:mainfrom
breken-ai:fix/check-instance-pep604-union

Conversation

@breken-ai

Copy link
Copy Markdown
Contributor

With lifecycle.FunctionInputOutputTypeChecker enabled, any node whose input or return annotation is a PEP 604 union with a parameterized member, such as list[int] | None, crashes the run. This happens even when the value is correct.

def evens(n: int) -> list[int] | None:
    return [i * 2 for i in range(n)] if n else None

dr = (
    driver.Builder()
    .with_modules(my_module)
    .with_adapters(lifecycle.FunctionInputOutputTypeChecker())
    .build()
)
dr.execute(["evens"], inputs={"n": 3})
# on main (Python 3.10-3.13):
#   File "hamilton/htypes.py", in check_instance
#     return isinstance(obj, type_)
# TypeError: isinstance() argument 2 cannot be a parameterized generic

htypes.check_instance detects unions through type_.__origin__ is Union. A types.UnionType (the X | Y form) has no __origin__ on Python 3.10-3.13, so it skips the union branch and reaches isinstance(obj, type_). isinstance raises when a union member is a parameterized generic. Optional[list[int]] works because it is a typing.Union, so only the | spelling crashes. On 3.14, X | Y and Union[X, Y] are the same type, so it no longer crashes there.

Changes

  • hamilton/htypes.py: in check_instance, check each member of a types.UnionType recursively, the same way typing.Union is already handled.
  • tests/test_type_utils.py: test_check_instance_with_pep604_union_of_generics. It covers matching and non-matching values for list[int] | None and dict[str, int] | list[int].
  • tests/lifecycle/test_default.py: two driver tests with FunctionInputOutputTypeChecker. The first uses list[int] | None as a node's return type and as a downstream input, for both a list and None. The second checks that a wrong result (["not", "ints"]) is still rejected with the adapter's own TypeError.

How I tested this

  • On unmodified main (4f4c48fe, Python 3.12): all 3 new tests fail with TypeError: isinstance() argument 2 cannot be a parameterized generic.
  • With the fix, all 3 pass. They also pass on Python 3.10 and 3.14.
  • pytest tests/test_type_utils.py tests/test_hamilton_driver.py tests/test_end_to_end.py tests/test_async_driver.py tests/test_graph.py tests/function_modifiers tests/test_function_modifiers.py tests/lifecycle tests/test_base.py tests/execution tests/io tests/caching tests/test_default_data_quality.py tests/test_node.py tests/test_ad_hoc_utils.py tests/test_parallel_graceful.py on Python 3.12: 1185 passed, 42 failed. The same 42 fail on main without this change (1182 passed): the test_graph.py display tests need graphviz, and the test_fingerprinting.py hash tests fail in my local env. None of them touch this code.
  • ruff check and ruff format --check (0.16.9, the pre-commit pin) are clean. git diff --check is clean.

Notes

A companion PR, #1735, fixes htypes.check_input_type rejecting dict inputs for Mapping[...] parameters. The two PRs touch different functions and do not conflict.

Checklist

  • PR has an informative and human-readable title (this will be pulled into the release notes)
  • Changes are limited to a single goal (no scope creep)
  • Code passed the pre-commit check & code is left cleaner/nicer than when first encountered.
  • Any change in functionality is tested
  • New functions are documented (with a description, list of inputs, and expected output) — no new functions
  • Placeholder code is flagged / future TODOs are captured in comments — none added
  • Project documentation has been updated if adding/changing functionality — no documented behavior changes

AI disclosure (per the ASF Generative Tooling guidance): this change was written with an AI coding tool (Claude Code, Claude Opus 5.5) working through the breken-ai account. The tool found the bug, wrote the fix and the tests, and wrote this description. The commit carries a Generated-by: trailer. The diff is a small original change to existing Hamilton code and includes no third-party material. The red/green runs above are real and can be re-run from the diff. If you would rather not take AI-assisted contributions here, say so and I will close this.

🤖 Generated with Claude Code

htypes.check_instance looked up `__origin__` to detect unions, but PEP 604
unions (types.UnionType) have none on Python 3.10-3.13, so the function fell
through to isinstance(obj, list[int] | None), which raises
"TypeError: isinstance() argument 2 cannot be a parameterized generic".
With FunctionInputOutputTypeChecker enabled, any node annotated with such a
union (input or return) crashed the run even when the value was correct.

Check each member of a types.UnionType, as is already done for
typing.Union.

Generated-by: Claude Code (Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@skrawcz
skrawcz force-pushed the fix/check-instance-pep604-union branch from 25e5184 to 6e4ec8f Compare September 26, 2026 18:57
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.

2 participants