Skip to main content

polytools

PyPI Python CI License: MIT Zero dependencies

Cross-provider LLM tool schema generation from Python type hints.

Write your tool once. Export to OpenAI, Anthropic, Gemini, and MCP — no framework lock-in, no third-party dependencies.

from polytools import tool

@tool
def search_web(query: str, max_results: int = 5) -> list[str]:
    """Search the web and return relevant URLs.

    Args:
        query: The search query string.
        max_results: Maximum number of results to return.
    """
    ...

search_web.to_openai()     # → OpenAI function calling format
search_web.to_anthropic()  # → Anthropic tools format
search_web.to_gemini()     # → Gemini FunctionDeclaration format
search_web.to_mcp()        # → MCP JSON-RPC tool definition
search_web.to_all()        # → all four at once

Why polytools?

Every LLM provider uses a different JSON format for tool/function definitions. Today you either hand-write four separate schemas, lock into a framework that weighs megabytes, or wrestle Pydantic into every project.

polytools is a single decorator — pure Python stdlib, zero external dependencies — that reads your type hints and docstring once and outputs whatever format you need.

Installation

pip install polytools

Requires Python 3.9+. No other dependencies, ever.

Quick Start

from polytools import tool
from typing import Optional, Literal

@tool
def send_email(
    recipient: str,
    subject: str,
    body: str,
    cc: Optional[str] = None,
    priority: Literal["low", "normal", "high"] = "normal",
) -> bool:
    """Send an email message.

    Args:
        recipient: Email address of the primary recipient.
        subject: Subject line of the email.
        body: Full text body of the email.
        cc: Optional CC email address.
        priority: Message priority level.
    """
    ...

OpenAI

import openai

client = openai.OpenAI()
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Send an email to alice@example.com"}],
    tools=[send_email.to_openai()],
)
Schema output
{
  "type": "function",
  "function": {
    "name": "send_email",
    "description": "Send an email message.",
    "parameters": {
      "type": "object",
      "properties": {
        "recipient": {"type": "string", "description": "Email address of the primary recipient."},
        "subject":   {"type": "string", "description": "Subject line of the email."},
        "body":      {"type": "string", "description": "Full text body of the email."},
        "cc":        {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional CC email address."},
        "priority":  {"type": "string", "enum": ["low", "normal", "high"], "description": "Message priority level."}
      },
      "required": ["recipient", "subject", "body"]
    }
  }
}

Anthropic

import anthropic

client = anthropic.Anthropic()
response = client.messages.create(
    model="claude-sonnet-5",
    messages=[{"role": "user", "content": "Send an email to alice@example.com"}],
    tools=[send_email.to_anthropic()],
)
Schema output
{
  "name": "send_email",
  "description": "Send an email message.",
  "input_schema": {
    "type": "object",
    "properties": {
      "recipient": {"type": "string", "description": "Email address of the primary recipient."},
      "subject":   {"type": "string", "description": "Subject line of the email."},
      "body":      {"type": "string", "description": "Full text body of the email."},
      "cc":        {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional CC email address."},
      "priority":  {"type": "string", "enum": ["low", "normal", "high"], "description": "Message priority level."}
    },
    "required": ["recipient", "subject", "body"]
  }
}

Gemini

import google.generativeai as genai

model = genai.GenerativeModel(
    model_name="gemini-1.5-pro",
    tools=[{"function_declarations": [send_email.to_gemini()]}],
)
Schema output
{
  "name": "send_email",
  "description": "Send an email message.",
  "parameters": {
    "type": "OBJECT",
    "properties": {
      "recipient": {"type": "STRING", "description": "Email address of the primary recipient."},
      "subject":   {"type": "STRING", "description": "Subject line of the email."},
      "body":      {"type": "STRING", "description": "Full text body of the email."},
      "cc":        {"type": "STRING", "description": "Optional CC email address."},
      "priority":  {"type": "STRING", "enum": ["low", "normal", "high"], "description": "Message priority level."}
    },
    "required": ["recipient", "subject", "body"]
  }
}

MCP

# In your MCP server's tools/list handler:
tools = [send_email.to_mcp()]
Schema output
{
  "name": "send_email",
  "description": "Send an email message.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "recipient": {"type": "string", "description": "Email address of the primary recipient."},
      "subject":   {"type": "string", "description": "Subject line of the email."},
      "body":      {"type": "string", "description": "Full text body of the email."},
      "cc":        {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional CC email address."},
      "priority":  {"type": "string", "enum": ["low", "normal", "high"], "description": "Message priority level."}
    },
    "required": ["recipient", "subject", "body"]
  }
}

Invoking from an LLM response

import json

# OpenAI
tool_call = response.choices[0].message.tool_calls[0]
result = send_email.call(json.loads(tool_call.function.arguments))

# Anthropic
block = response.content[0]  # ToolUseBlock
result = send_email.call(block.input)

Supported Type Annotations

Python type JSON Schema output
str {"type": "string"}
int {"type": "integer"}
float {"type": "number"}
bool {"type": "boolean"}
bytes {"type": "string", "format": "byte"}
None / type(None) {"type": "null"}
list / List[T] / list[T] {"type": "array", "items": {...}}
dict / Dict[K, V] / dict[K, V] {"type": "object", "additionalProperties": {...}}
tuple[T, ...] {"type": "array", "items": {...}}
tuple[T1, T2, T3] {"type": "array", "prefixItems": [...]}
set[T] / frozenset[T] {"type": "array", "uniqueItems": true, "items": {...}}
Optional[T] {"anyOf": [{...}, {"type": "null"}]}
Union[T1, T2] {"anyOf": [{...}, {...}]}
Literal["a", "b"] {"type": "string", "enum": ["a", "b"]}
Enum subclass {"type": <value type>, "enum": [...]}
dataclass {"type": "object", "properties": {...}, "required": [...]}
TypedDict {"type": "object", "properties": {...}, "required": [...]}
NamedTuple {"type": "object", "properties": {...}, "required": [...]}
Any {} (no constraints)
Unannotated {} (no constraints)

Nested types are fully supported: list[dict[str, Optional[int]]] works as expected.

Structured inputs

Parameters typed as a dataclass, TypedDict, NamedTuple, or Enum become proper nested JSON Schema objects — no Pydantic required. A field is marked required unless it has a default (or is a total=False / NotRequired TypedDict key).

from dataclasses import dataclass
from enum import Enum
from polytools import tool

class Priority(Enum):
    LOW = "low"
    HIGH = "high"

@dataclass
class Ticket:
    title: str
    priority: Priority
    assignee: str = ""

@tool
def create_ticket(ticket: Ticket) -> str:
    """Open a support ticket.

    Args:
        ticket: The ticket to create.
    """
    ...

create_ticket.to_openai()
Schema output (parameters)
{
  "type": "object",
  "properties": {
    "ticket": {
      "type": "object",
      "description": "The ticket to create.",
      "properties": {
        "title":    {"type": "string"},
        "priority": {"type": "string", "enum": ["low", "high"]},
        "assignee": {"type": "string"}
      },
      "required": ["title", "priority"]
    }
  },
  "required": ["ticket"]
}

Nesting is recursive (a dataclass field that is itself a dataclass, list[SomeDataclass], Optional[SomeTypedDict], dict[str, SomeEnum], …). Self-referential types are guarded and collapse to a bare {"type": "object"}.

Note: .call(args) passes the LLM's argument dict straight to your function; it does not reconstruct dataclass/TypedDict instances from that dict. Schema generation and argument coercion are separate concerns — coercion may land in a future release.

Docstring Styles

Parameter descriptions are parsed automatically from Google, NumPy, and reStructuredText docstrings:

Google (recommended)

def f(x: int, y: str = "hello") -> bool:
    """Summary line.

    Args:
        x: Description of x.
        y: Description of y.
    """

NumPy

def f(x: int, y: str = "hello") -> bool:
    """Summary line.

    Parameters
    ----------
    x : int
        Description of x.
    y : str, optional
        Description of y.
    """

reStructuredText

def f(x: int, y: str = "hello") -> bool:
    """Summary line.

    :param x: Description of x.
    :param y: Description of y.
    """

API Reference

@tool

Decorator that wraps a Python function and exposes provider schema methods. Can be used with or without parentheses:

@tool
def my_func(...): ...

@tool()
def my_func(...): ...

Tool methods

Method Returns Description
.to_openai() dict OpenAI Chat Completions tools list entry
.to_anthropic() dict Anthropic Messages API tools list entry
.to_gemini() dict Gemini function_declarations list entry
.to_mcp() dict MCP tools/list response entry
.to_all() dict[str, dict] All four, keyed by provider name
.call(args: dict) Any Invoke the function with an LLM argument dict

Tool attributes

Attribute Type Description
__wrapped__ Callable The original unwrapped function
__name__ str Preserved from the original function
__doc__ str Preserved from the original function

Notes on Gemini

Gemini's API does not support anyOf in tool schemas. Optional[T] parameters are handled by omitting them from the required list rather than encoding nullability in the schema type. Union types are resolved to the first non-null type.

Contributing

See CONTRIBUTING.md.

License

MIT — see LICENSE.


Part of the aenealabs AI agent toolkit.

Metadata

Release files for polytools 0.2.1

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

Source distribution (sdist)

Source distribution for polytools 0.2.1
File Size Uploaded
polytools-0.2.1.tar.gz 29.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for polytools 0.2.1
File Interpreter ABI Platform
polytools-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 51.2 kB

Release files / polytools-0.2.1.tar.gz

Download URL polytools-0.2.1.tar.gz
Size 29.0 kB
Tags Source
SHA-256 checksum
How to use checksums
7ebdc2d31a714365c6afef1d6780b2a27c243c5d8e9b18f330762901691d54b9
BLAKE2b-256 checksum
How to use checksums
c8fcbcde289fd091dab62d30603022b5286ccfe59f6bc0064fdba9e133b9c739
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.2.0 CPython/3.12.13

Release files / polytools-0.2.1-py3-none-any.whl

Download URL polytools-0.2.1-py3-none-any.whl
Size 22.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a51ac0d8ea7f1f7d104c024f2ed6c7d5a967d9968b3f7e5b72ac59a8a3b990be
BLAKE2b-256 checksum
How to use checksums
855f5745866eda16908105577a291cc6b24c4bdb2ac4e1a098a923c72464ac0a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.2.0 CPython/3.12.13

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

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