Python: Fix executor handler type resolution when using from __future__ import annotations#4317
Conversation
microsoft#3898) Use typing.get_type_hints() in _validate_handler_signature to resolve string annotations from `from __future__ import annotations`. This mirrors the fix applied to FunctionExecutor in microsoft#2308. When __future__ annotations are enabled, type annotations are stored as strings. The handler decorator was passing these strings directly to validate_workflow_context_annotation, which uses typing.get_origin and returns None for strings, causing a ValueError. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 85%
✓ Correctness
This diff correctly fixes handling of PEP 563 stringified annotations (from
from __future__ import annotations) by usingtyping.get_type_hints()to resolve forward references. The implementation properly falls back toinspect.Parameter.annotationwhen a parameter name isn't found in the resolved hints, and the test coverage is solid across key scenarios. No correctness issues found.
✓ Security Reliability
The change correctly uses
typing.get_type_hints()to resolve stringified annotations fromfrom __future__ import annotations. There are no security issues—whileget_type_hints()internally evaluates string annotations, these originate from developer-authored source code, not untrusted input. The main reliability concern is thatget_type_hints()can raiseNameError(or other exceptions) when forward references cannot be resolved, and there is no error handling around the call, which could produce confusing tracebacks during handler registration.
✓ Test Coverage
The new test file covers the main happy paths for future annotations (multi-arg, single-arg, complex types, bare context), which is good. However, there are two notable gaps: (1) the
skip_message_annotationcode path (when@handler(input=..., output=...)is used) was also changed to use resolved type hints but has no corresponding future-annotations test, and (2) the bare-context test only asserts handler registration without verifying thatoutput_typesandworkflow_output_typesare correctly empty, making it weaker than the other tests.
Suggestions
- Consider wrapping
typing.get_type_hints(func)in a try/except forNameError(unresolvable forward references) to provide a more user-friendly error message pointing to the handler and the unresolvable annotation, rather than letting the rawNameErrorpropagate. - Consider wrapping
typing.get_type_hints(func)in a try/except to catchNameErrororException, falling back to rawinspectannotations and/or raising a more descriptiveValueErrorthat mentions the handler name and hints at the unresolvable type. This would improve the developer experience when a forward reference cannot be resolved at registration time. - Add a test for
@handler(input=MyTypeA, output=MyTypeB)withfrom __future__ import annotationsto cover theskip_message_annotationbranch that was also modified in_validate_handler_signature. - Strengthen the bare-context test to assert on
output_typesandworkflow_output_types, consistent with the other tests in the suite. - Consider adding a test for union-type context annotations (e.g.,
WorkflowContext[MyTypeA | MyTypeB, MyTypeC]) with future annotations, since union handling is a common source of edge-case bugs with stringified annotations.
Automated review by moonbox3's agents
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug where the @handler decorator in Python Executor classes fails when from __future__ import annotations is used (which becomes standard in Python 3.12+). The issue occurs because stringified annotations need to be resolved to actual type objects before validation, but the original code expected resolved types directly from inspect.Parameter.annotation.
Changes:
- Added
typing.get_type_hints()call to resolve stringified annotations before type validation in_validate_handler_signature() - Updated message type and context annotation resolution to use resolved type hints with proper fallback
- Added comprehensive test suite to verify the fix works across different annotation patterns
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
python/packages/core/agent_framework/_workflows/_executor.py |
Fixed _validate_handler_signature() to resolve stringified annotations using typing.get_type_hints() before validation, matching the pattern already used in FunctionExecutor |
python/packages/core/tests/workflow/test_executor_future.py |
Added new test module with from __future__ import annotations to verify handler decorator works correctly with stringified annotations across multiple scenarios (two type args, single type arg, complex types, bare context) |
… and test coverage - Wrap typing.get_type_hints() in try/except to provide a descriptive ValueError mentioning the handler name when annotations cannot be resolved - Strengthen bare context test to assert output_types and workflow_output_types - Add test for @handler(input=..., output=...) with future annotations covering the skip_message_annotation branch - Add test for union-type context annotations with future annotations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rosoft#3898) - Narrow except clause from bare Exception to (NameError, AttributeError, TypeError) to avoid masking unexpected errors. - Add test_handler_unresolvable_annotation_raises to verify that a handler with a forward-reference to a non-existent type raises ValueError with the expected message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fails When typing.get_type_hints(func) raises NameError (unresolvable forward ref), AttributeError, RecursionError, or any other exception, fall back to the raw parameter annotations instead of raising a ValueError. This matches the suggestion from @moonbox3 on PR microsoft#4317. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…crosoft#3898) The code now falls back to raw string annotations instead of raising 'Failed to resolve type annotations'. A ValueError is still raised when the raw string ctx annotation is not a valid WorkflowContext type, so update the test to match on ValueError without checking the message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
When
from __future__ import annotationsis used (standard in Python 3.12+), all annotations become strings instead of resolved types. This causes_validate_handler_signatureto fail because it compares string annotations against actual types, breaking@handlerdecorator validation in executors.Fixes #3898
Description
The root cause is that
inspect.Parameter.annotationreturns raw string annotations whenfrom __future__ import annotationsis active, but the validation logic expected resolved type objects. The fix usestyping.get_type_hints(func)to eagerly resolve stringified annotations back to their actual types before performing type checks on thectxandmessageparameters. A new test module withfrom __future__ import annotationsenabled at the module level verifies that handler registration works correctly with stringified annotations across multiple scenarios.Contribution Checklist