Skip to content

Fix durableClient binding FunctionLoadError on Python 3.14 (PEP 649/749)#611

Open
nachogarcia wants to merge 1 commit into
Azure:devfrom
nachogarcia:fix/py314-durable-client-input-annotation
Open

Fix durableClient binding FunctionLoadError on Python 3.14 (PEP 649/749)#611
nachogarcia wants to merge 1 commit into
Azure:devfrom
nachogarcia:fix/py314-durable-client-input-annotation

Conversation

@nachogarcia

Copy link
Copy Markdown

Problem

On Python 3.14, a function app that uses @app.durable_client_input(...) fails to start: the durableClient binding fails type validation in the Functions Python worker, raising FunctionLoadError, and the host returns 503. The same app works on Python ≤3.13.

Fixes #596.

Root cause

DFApp._add_rich_client makes the worker's binding type-check pass by forcing the client parameter's annotation to str:

user_code.__annotations__[parameter_name] = str

…and then wraps the user function with @functools.wraps(user_code) into df_client_middleware.

PEP 649 / PEP 749 (Python 3.14) changed how annotations are stored: functions now carry an __annotate__ callable, and functools.WRAPPER_ASSIGNMENTS copies __annotate__ instead of the __annotations__ dict. The copied __annotate__ lazily re-derives the original DurableOrchestrationClient annotation, overriding the str patch. The worker reads the wrapper's annotation as DurableOrchestrationClient, durableClient binding validation fails, and startup 503s.

On Python ≤3.13 there is no __annotate__, so wraps copied the already-patched __annotations__ dict and everything worked.

Fix

After the @wraps wrapper is created, re-apply the str override on the wrapper and drop its __annotate__, so both the dict-based reader and the __annotate__-based reader (used by the worker on 3.14) agree:

_ann = dict(getattr(df_client_middleware, "__annotations__", {}))
_ann[parameter_name] = str
df_client_middleware.__annotations__ = _ann
if hasattr(df_client_middleware, "__annotate__"):
    df_client_middleware.__annotate__ = None

The pre-wrap patch is kept for the ≤3.13 path. The block is a guarded no-op on ≤3.13 (hasattr(__annotate__) is False there). Runtime behavior is unchanged: the rich DurableOrchestrationClient is still constructed from the starter string and passed to user code — only the annotation the worker inspects is forced to str.

Testing

  • New regression test test_durable_client_input_annotation_overridden_to_str asserts the built wrapper's client-parameter annotation reads as str via both __annotations__ and (on 3.14) annotationlib.get_annotations(..., format=Format.FORWARDREF). It fails on 3.14 without the fix and passes with it.
  • Full suite: 310 passed on Python 3.14.6.
  • Decorator tests also pass on Python 3.13.7 (no regression on the pre-PEP-649 path).
  • flake8 ./azure/ clean.

🤖 Generated with Claude Code

`_add_rich_client` makes the worker's `durableClient` binding type-check
pass by forcing the client parameter's annotation to `str`
(`user_code.__annotations__[parameter_name] = str`) before wrapping the
user function with `@functools.wraps(user_code)`.

On Python 3.14, PEP 649/749 changed how annotations are carried:
`functools.WRAPPER_ASSIGNMENTS` now copies `__annotate__` instead of the
`__annotations__` dict. The copied `__annotate__` lazily re-derives the
*original* `DurableOrchestrationClient` annotation, overriding the `str`
patch. The worker then reads the wrapper's annotation as
`DurableOrchestrationClient`, `durableClient` binding validation fails,
`FunctionLoadError` is raised, and the host 503s on startup. On Python
<=3.13 this worked because `wraps` copied the already-patched
`__annotations__` dict.

Re-apply the `str` override on the wrapper after `wraps` runs, and drop
`__annotate__` so both the dict-based reader and the `__annotate__`-based
reader (which the worker uses on 3.14) agree. The pre-wrap patch is kept
for the <=3.13 path. The rich `DurableOrchestrationClient` is still
constructed at call time and passed to user code unchanged.

Adds a regression test asserting the built wrapper's client-parameter
annotation reads as `str` via both `__annotations__` and, on 3.14,
`annotationlib.get_annotations(..., format=FORWARDREF)`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nachogarcia

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Refine-Technologies-Inc"

@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

@nachogarcia the command you issued was incorrect. Please try again.

Examples are:

@microsoft-github-policy-service agree

and

@microsoft-github-policy-service agree company="your company"

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.

Python3.14 function app startup error due to type hint issue

1 participant