Skip to main content

LLM Pydantic Tools

LLM Pydantic Tools banner

Turn Pydantic models into function-calling tools. llm-pydantic-tools converts a model, or a JSON schema, into the tool format of the main LLM APIs (OpenAI Chat Completions and Responses, Anthropic and Gemini), builds the matching tool_choice value, and validates the arguments the model sends back.

CI PyPI version Python versions License: MIT DeepWiki

Features

  • Model to tool: converts a Pydantic model, or a JSON schema, into a function tool for OpenAI (Chat Completions or Responses), Anthropic or Gemini. The model's docstring becomes the tool description, unless you pass description.
  • Lean schemas: strips the title that Pydantic adds to every schema, without touching fields or values that are also called title.
  • Valid tool names: names the tool after the model, or after tool_name, and checks the name against the API rules: 1 to 64 ASCII letters, digits, underscores or dashes.
  • tool_choice values: auto, required, none, or forcing this tool, in the format of each API.
  • Answer validation: checks the arguments the model returns against the tool's schema.
  • Typed parsing: turns the arguments into an instance of your model, running its Pydantic validators.

Installation

uv add llm-pydantic-tools

Or with pip:

pip install llm-pydantic-tools

Usage

Define a tool

from typing import Literal

from pydantic import BaseModel, Field

from llm_pydantic_tools import ToolChoiceEnum, ToolSchemaManager, get_tool_choice_dict


class GetWeather(BaseModel):
    """Get the current weather in a city."""

    city: str = Field(description="City name, e.g. Montevideo")
    unit: Literal["celsius", "fahrenheit"] = "celsius"


weather_tool = ToolSchemaManager(pydantic_obj=GetWeather, tool_name="get_weather")

Without tool_name, the tool is named after the model (GetWeather).

weather_tool.tools_schema holds the tool, ready to send:

[
  {
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "Get the current weather in a city.",
      "parameters": {
        "properties": {
          "city": {
            "description": "City name, e.g. Montevideo",
            "type": "string"
          },
          "unit": {
            "default": "celsius",
            "enum": [
              "celsius",
              "fahrenheit"
            ],
            "type": "string"
          }
        },
        "required": [
          "city"
        ],
        "type": "object"
      }
    }
  }
]

Call the API

Pass tools_schema as tools, and the result of get_tool_choice_dict() as tool_choice. With the OpenAI SDK:

from openai import OpenAI

client = OpenAI()

completion = client.chat.completions.create(
    model="gpt-6-astra",
    messages=[{"role": "user", "content": "What's the weather in Montevideo?"}],
    tools=weather_tool.tools_schema,
    tool_choice=get_tool_choice_dict(ToolChoiceEnum.TOOL_NAME, weather_tool),
)
tool_call = completion.choices[0].message.tool_calls[0]

ToolChoiceEnum.TOOL_NAME forces the model to call this tool. AUTO, REQUIRED and NONE map to "auto", "required" and "none".

Validate the answer

The model does not always follow the schema, so check the arguments before using them. validate() raises a jsonschema.ValidationError that describes the problem:

import json

from jsonschema import ValidationError

arguments = json.loads(tool_call.function.arguments)

try:
    weather_tool.validate(arguments)
except ValidationError as error:
    print(error.message)  # e.g. 'kelvin' is not one of ['celsius', 'fahrenheit']

If you only need a yes or no, weather_tool.is_valid(arguments) returns True or False.

validate_tool_answer(), from earlier versions, still returns True or the error instead of raising it. Compare its result with is True: the error counts as true in an if.

Get an instance of the model

validate() checks the arguments against the JSON schema, so it also works when you start from a schema. If you built the tool from a model, parse() goes further: it runs Pydantic, with your validators and type conversions, and returns an instance of the model. It takes the arguments as a JSON string or already parsed:

weather = weather_tool.parse(tool_call.function.arguments)  # a GetWeather
print(weather.city)

If the arguments don't fit the model, it raises a pydantic.ValidationError.

Start from a JSON schema

If you already have the JSON schema, pass it instead of the model. The tool is named after the schema's title; if it has none, pass tool_name:

schema_manager = ToolSchemaManager(
    pydantic_obj_json_schema=my_json_schema,
    tool_name="my_tool",
)

Other formats

tools_schema holds the tool in the Chat Completions format. tool() returns it in the format of another API, and format_tools() builds the tools list of a request from several managers. get_tool_choice_dict() takes the same format as its third argument:

ToolFormat API Pass get_tool_choice_dict() as
CHAT_COMPLETIONS (default) OpenAI Chat Completions, and compatible APIs such as Mistral's and Ollama's tool_choice
RESPONSES OpenAI Responses API tool_choice
ANTHROPIC Anthropic's Messages API (Claude) tool_choice
GEMINI Gemini's generate_content tool_config
GEMINI_INTERACTIONS Gemini's Interactions API generation_config["tool_choice"]

The tools always go in tools. For Gemini's generate_content, format_tools() groups them in a single function_declarations list, as that API expects, and in every format it rejects two tools with the same name.

With Anthropic's SDK:

import anthropic

from llm_pydantic_tools import ToolFormat

client = anthropic.Anthropic()

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    messages=[{"role": "user", "content": "What's the weather in Montevideo?"}],
    tools=[weather_tool.tool(ToolFormat.ANTHROPIC)],
    tool_choice=get_tool_choice_dict(ToolChoiceEnum.AUTO, weather_tool, ToolFormat.ANTHROPIC),
)
for block in message.content:
    if block.type == "tool_use":
        weather_tool.validate(block.input)

where weather_tool.tool(ToolFormat.ANTHROPIC) is:

{
  "name": "get_weather",
  "description": "Get the current weather in a city.",
  "input_schema": {
    "properties": {
      "city": {
        "description": "City name, e.g. Montevideo",
        "type": "string"
      },
      "unit": {
        "default": "celsius",
        "enum": [
          "celsius",
          "fahrenheit"
        ],
        "type": "string"
      }
    },
    "required": [
      "city"
    ],
    "type": "object"
  }
}

With Gemini's generate_content:

from google import genai
from google.genai import types

from llm_pydantic_tools import format_tools

client = genai.Client()

response = client.models.generate_content(
    model="gemini-flash-latest",
    contents="What's the weather in Montevideo?",
    config=types.GenerateContentConfig(
        tools=format_tools([weather_tool], ToolFormat.GEMINI),
        tool_config=get_tool_choice_dict(ToolChoiceEnum.TOOL_NAME, weather_tool, ToolFormat.GEMINI),
    ),
)
weather_tool.validate(response.function_calls[0].args)

Compatibility

The tools are plain dicts, so they work with each provider's SDK or with raw HTTP requests. Keep in mind:

  • The schemas are not prepared for strict mode (additionalProperties: false on every object and every field in required). If you use the OpenAI Python SDK and need strict mode, openai.pydantic_function_tool() covers that case.
  • The newest Claude models (Opus 5.5, Sonnet 5.5 and Fable 5.1) reject forcing a tool, so REQUIRED and TOOL_NAME fail there. Use AUTO and ask for the tool in the prompt.
  • Gemini asks for a description of every function, and supports a subset of JSON Schema.
  • Tool names follow the strictest rule of these APIs (Chat Completions'), so a valid name works with all of them.

llm-pydantic-tools only depends on Pydantic and jsonschema, which makes it handy when you build the requests yourself or switch between providers.

Contributing

Contributions are welcome! Please feel free to submit pull requests, create issues, and suggest improvements to the repository.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Metadata

Release files for llm-pydantic-tools 0.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for llm-pydantic-tools 0.4
File Size Uploaded
llm_pydantic_tools-0.4.tar.gz 17.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for llm-pydantic-tools 0.4
File Interpreter ABI Platform
llm_pydantic_tools-0.4-py3-none-any.whl Python 3 none any Details

Total release size: 28.6 kB

Release files / llm_pydantic_tools-0.4.tar.gz

Download URL llm_pydantic_tools-0.4.tar.gz
Size 17.8 kB
Tags Source
SHA-256 checksum
How to use checksums
67c00edd01bdd6d2f766431f29e0e431c62d30c3c92301e9463e7a8b16642efd
BLAKE2b-256 checksum
How to use checksums
ab03456a9634c42a9774bd554d1e6c7c8fc3f3692bd0c9e2019319c9aaeee56c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / llm_pydantic_tools-0.4-py3-none-any.whl

Download URL llm_pydantic_tools-0.4-py3-none-any.whl
Size 10.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3a21509a958a638fd9e985d49c78ce65d6d8bad33ea6947a54db0a56d57ffb71
BLAKE2b-256 checksum
How to use checksums
9658ef31b8f0316e1e876e6955293929407d19324ac26f314f79df27745b024c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.4 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page