Skip to main content

schema2code

Convert LLM tool schemas into a compact, sandboxed Python code interface.

PyPI Python License Tests Dependencies

English · 한국어 · 日本語


Instead of resending JSON Schemas on every turn, hand the model short function signatures and let it write one Python program that calls your tools inside a restricted sandbox.

Traditional:  [JSON schemas, every turn] → tool-call JSON → execute → repeat
schema2code:  [compact signatures, once] → Python code → sandbox → result

[!TIP] The win is not only a smaller prompt — it is fewer round trips. One program replaces a chain of tool calls, so a task that cost four API turns costs one, and the schemas stop being re-sent with each turn.

Measured, not estimated

Live A/B against the OpenAI API — 15 tasks × 2 arms, gpt-4o-mini ×3 repeats + gpt-4o ×1, temperature 0, token counts taken from the API usage field:

JSON tool-calling schema2code delta
Tokens per task 3,319 995 −70%
API round trips 3.35 1.15 −66%
Answer accuracy 61.7% 78.3% +16.7pt
… gpt-4o only 80% 100%
Cost $0.138 $0.054 −61%
Sandbox-guard false positives 0 / 60 runs

The gap widens with task complexity: loop-style tasks drop from 4.2 round trips to 1.0, and the hardest task saved 89% of tokens. Full method, per-task tables, and raw transcripts live in benchmarks/results/ — every run writes report.md, transcript.jsonl, and runs.json.

[!NOTE] To be fair about the other side: with only 2 small tools the compact interface costs 11 tokens more than the schemas — the fixed usage-rules text dominates. Savings turn clearly positive around 10+ tools. See Token measurement.

Install

pip install schema2code
# optional extras
pip install "schema2code[tiktoken]"   # accurate token counts
pip install "schema2code[openai]"     # agent-loop example / integration tests

Zero runtime dependencies. Python ≥ 3.10, Windows/macOS/Linux.

Sixty seconds

from schema2code import ToolRegistry, Sandbox

registry = ToolRegistry()

@registry.tool
def get_weather(city: str, unit: str = "celsius") -> dict:
    """Return current weather for a city."""
    return {"city": city, "temp": 20, "unit": unit}

@registry.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

sandbox = Sandbox(registry, timeout=8.0)

outcome = sandbox.run("""
w = get_weather("Berlin")
result = add(w["temp"], 2)
print("temp+2 =", result)
""")

outcome is a plain dataclass:

success       True
result        22
stdout        'temp+2 = 22\n'
tools_called  ['get_weather', 'add']
duration      0.0002

And this is the entire prompt block the model needs — the output of registry.to_prompt():

# Available tools
get_weather(city: str, unit: str = 'celsius') -> dict
    # Return current weather for a city.
add(a: int, b: int) -> int
    # Add two numbers.

## How to call tools

Write Python code that calls the functions below.
- Use only the listed tools and allowed standard modules.
- Prefer clear intermediate variables.
- Print values you want to inspect; assign the final value to `result`
  or leave it as the last expression.
- Do not import disallowed modules, open files, or access the network.

Failures come back classified, ready to feed to the model for a retry:

outcome = sandbox.run('import json\nresult = json.__builtins__')
outcome.error_type    # "security"
outcome.short_error   # "... Blocked by sandbox guard: attribute access to '__builtins__' is blocked (line 2) ..."

error_type is one of ok · syntax · timeout · tool · security · import · runtime · policy · validation.

[!TIP] That single line is the whole retry loop. Append short_error to the conversation and ask for another code block — in the live benchmark this converged in 1.15 API calls per task on average.

Already have schemas?

OpenAI and Anthropic tool definitions import directly, constraints included. Names that aren't valid Python identifiers (MCP-style set-temp) are sanitized so the generated code can actually call them:

registry.load_openai_tools(openai_tools, handlers={"set-temp": set_temp})
registry.load_anthropic_tools(anthropic_tools, handlers=...)
### set_temp
Parameters:
  - value: float (required) [minimum=0, maximum=100]
  - unit: str (required) [enum=['celsius', 'fahrenheit']]

With Sandbox(registry, validate_calls=True) those constraints are enforced at call time — set_temp(value=150, ...) fails with Invalid argument for set_temp.value: 150 > maximum 100 before your handler runs.

Sandbox levels

Level Class Isolation Use it for
0 RestrictedSandbox restricted builtins, same process notebooks, tests
1 Sandbox (default) child process + hard timeout + tool RPC local agent loops
2 DockerSandbox container (stub — no host tools) isolated pure-Python eval

The default Sandbox runs your tools in the parent process over an RPC channel, so closures, bound methods, and lambdas all work — nothing gets pickled. Timeouts kill the child; a hung host tool can't stall run() past its deadline either.

[!WARNING] This is not a hard security boundary. The guards block the known introspection escapes (().__class__.__base__.__subclasses__(), json.__builtins__, dynamic .format() templates, operator.attrgetter) and rejected 13/13 vectors in the regression suite, but a denylist over Python stays a denylist. Multi-tenant production needs OS/container isolation on top — read SECURITY.md before deploying.

Full feature list
Area What you get
Registration @registry.tool decorator, register(), module-level @tool
Signatures type hints + docstrings; imported schemas keep their constraints
Prompt styles signatures (cheapest), detailed (shows constraints), minimal
Tool RPC default path — host callables never need pickling
Isolation child process with hard timeout; deadline covers host tool time
Outcome success, result, stdout, error, duration, tools_called, error_type, short_error, truncation flags, cached, debug
Escape guards AST dunder rejection + SafeModule import proxies (guard=True)
Call policy max_tool_calls, max_total_calls, max_calls_per_tool
Validation validate_calls=True enforces enum / min / max / pattern
Schema import load_openai_tools, load_anthropic_tools (+ name sanitizing)
Metrics compare_tokens(), estimate_tokens() (tiktoken when installed)
Cache opt-in ResultCache, keyed on code + tools + security config
Async async def tools are awaited; await sandbox.run_async(code)
Debug debug=True → code hash, call counts, backend, duration
Public API surface
from schema2code import (
    ToolRegistry, tool, get_default_registry,
    Sandbox,            # process + restricted + RPC (default)
    RestrictedSandbox,  # in-process only
    ProcessSandbox, DockerSandbox, docker_available,
    Outcome, ToolSpec, CallPolicy, ResultCache,
    compare_tokens, estimate_tokens,
    load_openai_tools, load_anthropic_tools,
    SecurityError, PolicyError, ValidationError, UnsupportedError,
)

What this is not

  • Not an agent framework. No planner, memory, router, or chain. You own the loop — this library turns tools into a prompt block and runs the code that comes back. It sits underneath whatever framework you already use.
  • Not a security boundary for untrusted input. It defends against model mistakes, not against an adversary. See the warning above.
  • Not a win for every setup. Below roughly 10 tools the interface can cost more than the schemas it replaces.
  • Not a hosted runtime. Everything runs on your machine — no service, no account, no vendor.

The measured numbers come from one workload: 15 tasks over a 14-tool registry, OpenAI models, temperature 0. The direction should transfer — more tools and longer chains favour code — but treat the exact percentages as workload-specific and re-run benchmarks/bench_v2.py against your own tools.

Documentation

Usage guide docs/usage.md registration, prompt styles, retry loop, policies, caching, async
Architecture docs/architecture.md RPC protocol, guard design, cache keys, error taxonomy
Security model SECURITY.md threat model, backend levels, hard requirements
Releasing RELEASING.md build & publish checklist

한국어: docs/usage.ko.md · docs/architecture.ko.md / 日本語: docs/usage.ja.md · docs/architecture.ja.md

Development

git clone https://github.com/DW-dev-UE/schema2code.git && cd schema2code
pip install -e ".[dev]"
python -m pytest -q                  # 97 tests
python -m ruff check src benchmarks
python benchmarks/run_all.py         # local suite (no API key needed)
python benchmarks/bench_v2.py --dry-run   # live A/B cost preview

[!IMPORTANT] The live benchmark (bench_v2.py) calls a paid API and spends real money. It needs OPENAI_API_KEY, prints a cost estimate before the first call, and stops hard at --budget. Start with --dry-run.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

schema2code-0.2.0.tar.gz (44.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

schema2code-0.2.0-py3-none-any.whl (42.7 kB view details)

Uploaded Python 3

File details

Details for the file schema2code-0.2.0.tar.gz.

File metadata

  • Download URL: schema2code-0.2.0.tar.gz
  • Upload date:
  • Size: 44.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for schema2code-0.2.0.tar.gz
Algorithm Hash digest
SHA256 87951b9d927603996164b2b50d073b5d2b46535bf12a8a65e803ef1d09456b8d
MD5 90741a0e0e609213c62647bcec9495d9
BLAKE2b-256 412836ca73227764df39abbaccce7a05eb278f27f4cc375c997618d6ef422ccf

See more details on using hashes here.

Provenance

The following attestation bundles were made for schema2code-0.2.0.tar.gz:

Publisher: publish.yml on DW-dev-UE/schema2code

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file schema2code-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: schema2code-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 42.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for schema2code-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1991c5c7290f5b3865a924ee1606d5a7cc6cc5d83f25a06ae1f098bbbc13822b
MD5 da2de3d81e4b6a03472b3e5c38129495
BLAKE2b-256 65066abf3e6edf6445adcfc85c4f90ac0e633e9229c2da8e74645c5047167f5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for schema2code-0.2.0-py3-none-any.whl:

Publisher: publish.yml on DW-dev-UE/schema2code

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 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