polytools
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)
| File | Size | Uploaded | |
|---|---|---|---|
| polytools-0.2.1.tar.gz | 29.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|