Skip to content

3.14: Tool definition not working with sync_to_async #3685

@nairb774

Description

@nairb774

Initial Checks

Description

We have a few tool definitions that are wrapped with sync_to_async to make the boundary between pydantic-ai and the underlying Django layer work reliably. With Python 3.13 (and prior) this seems to work fine. Attempting to update to 3.14 causes the tool definitions to fail with some unique errors. From the sample below:

Traceback (most recent call last):
  File "script.py", line 38, in <module>
    Tool(foo)
    ~~~~^^^^^
  File ".cache/uv/environments-v2/script-b6576b051c3e2f31/lib/python3.14/site-packages/pydantic_ai/tools.py", line 358, in __init__
    self.function_schema = function_schema or _function_schema.function_schema(
                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        function,
        ^^^^^^^^^
    ...<3 lines>...
        require_parameter_descriptions=require_parameter_descriptions,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File ".cache/uv/environments-v2/script-b6576b051c3e2f31/lib/python3.14/site-packages/pydantic_ai/_function_schema.py", line 137, in function_schema
    annotation = type_hints[name]
                 ~~~~~~~~~~^^^^^^
KeyError: 'a'

From tracing down the code, it looks like the behavior of functools.update_wrapper 1 changed between 3.13 and 3.14 - specifically the __annotations__ wrapper assignment was swapped for __annotate__. This causes pydantic._internal._typing_extra.get_function_type_hints to return {} resulting in function_schema to explode.

I don't have an immediate need for 3.14 support, but this is the current issue we are seeing which is preventing any further evaluation.

Example Code

# /// script
# requires-python = ">=3.13"
# dependencies = [
#     # Direct dependencies:
#     "asgiref==3.11.0",
#     "pydantic-ai-slim==1.28.0",
#
#     # Transitive dependencies (aka "lock file")
#     "annotated-types==0.7.0",
#     "anyio==4.12.0",
#     "certifi==2025.11.12",
#     "colorama==0.4.6",
#     "genai-prices==0.0.47",
#     "griffe==1.15.0",
#     "h11==0.16.0",
#     "httpcore==1.0.9",
#     "httpx==0.28.1",
#     "idna==3.11",
#     "importlib-metadata==8.7.0",
#     "logfire-api==4.16.0",
#     "opentelemetry-api==1.39.0",
#     "pydantic==2.12.5",
#     "pydantic-core==2.41.5",
#     "pydantic-graph==1.28.0",
#     "typing-extensions==4.15.0",
#     "typing-inspection==0.4.2",
#     "zipp==3.23.0",
# ]
# ///

from asgiref.sync import sync_to_async
from pydantic_ai.tools import Tool

@sync_to_async
def foo(a: int, b: str) -> float:
  ...

Tool(foo)

Python, Pydantic AI & LLM client version

pydantic-ai-slim==1.28.0

Python 3.13.11 (works) vs 3.14.2 (fails)

Sample code above has all transitive dependencies spelled out as of right now and should be run with:


$ uv --version
uv 0.9.16 (a63e5b62e 2025-12-06)
$ uv run --python 3.13.11 script.py # No output == good
$ uv run --python 3.14.2 script.py # Exception :(

Footnotes

  1. Called from here.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions