Skip to content

Python: Fix executor handler type resolution when using from __future__ import annotations#4317

Merged
moonbox3 merged 9 commits intomicrosoft:mainfrom
moonbox3:agent/fix-3898-2
Feb 27, 2026
Merged

Python: Fix executor handler type resolution when using from __future__ import annotations#4317
moonbox3 merged 9 commits intomicrosoft:mainfrom
moonbox3:agent/fix-3898-2

Conversation

@moonbox3
Copy link
Contributor

Motivation and Context

When from __future__ import annotations is used (standard in Python 3.12+), all annotations become strings instead of resolved types. This causes _validate_handler_signature to fail because it compares string annotations against actual types, breaking @handler decorator validation in executors.

Fixes #3898

Description

The root cause is that inspect.Parameter.annotation returns raw string annotations when from __future__ import annotations is active, but the validation logic expected resolved type objects. The fix uses typing.get_type_hints(func) to eagerly resolve stringified annotations back to their actual types before performing type checks on the ctx and message parameters. A new test module with from __future__ import annotations enabled at the module level verifies that handler registration works correctly with stringified annotations across multiple scenarios.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Note: PR autogenerated by moonbox3's agent

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>
Copilot AI review requested due to automatic review settings February 26, 2026 12:31
Copy link
Contributor Author

@moonbox3 moonbox3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 3 | Confidence: 85%

✓ Correctness

This diff correctly fixes handling of PEP 563 stringified annotations (from from __future__ import annotations) by using typing.get_type_hints() to resolve forward references. The implementation properly falls back to inspect.Parameter.annotation when 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 from from __future__ import annotations. There are no security issues—while get_type_hints() internally evaluates string annotations, these originate from developer-authored source code, not untrusted input. The main reliability concern is that get_type_hints() can raise NameError (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_annotation code 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 that output_types and workflow_output_types are correctly empty, making it weaker than the other tests.

Suggestions

  • Consider wrapping typing.get_type_hints(func) in a try/except for NameError (unresolvable forward references) to provide a more user-friendly error message pointing to the handler and the unresolvable annotation, rather than letting the raw NameError propagate.
  • Consider wrapping typing.get_type_hints(func) in a try/except to catch NameError or Exception, falling back to raw inspect annotations and/or raising a more descriptive ValueError that 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) with from __future__ import annotations to cover the skip_message_annotation branch that was also modified in _validate_handler_signature.
  • Strengthen the bare-context test to assert on output_types and workflow_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

@markwallace-microsoft
Copy link
Member

markwallace-microsoft commented Feb 26, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework/_workflows
   _executor.py1831094%211, 335, 337, 346, 366, 369, 476, 481, 491, 650
TOTAL22222275887% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
4714 247 💤 0 ❌ 0 🔥 1m 18s ⏱️

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

moonbox3 and others added 2 commits February 26, 2026 21:37
… 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>
moonbox3 and others added 6 commits February 27, 2026 07:48
…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>
@moonbox3 moonbox3 added this pull request to the merge queue Feb 27, 2026
Merged via the queue into microsoft:main with commit 0d6b9d6 Feb 27, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Executor type checking fails when using __future__ annotations in Python 3.12

6 participants