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
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.
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.
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.
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
License
MIT
Metadata
Release files for schema2code 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| schema2code-0.2.0.tar.gz | 44.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| schema2code-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 87.0 kB
Release files / schema2code-0.2.0.tar.gz
| Download URL | schema2code-0.2.0.tar.gz |
|---|---|
| Size | 44.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
87951b9d927603996164b2b50d073b5d2b46535bf12a8a65e803ef1d09456b8d
|
|
BLAKE2b-256 checksum How to use checksums |
412836ca73227764df39abbaccce7a05eb278f27f4cc375c997618d6ef422ccf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency logRelease files / schema2code-0.2.0-py3-none-any.whl
| Download URL | schema2code-0.2.0-py3-none-any.whl |
|---|---|
| Size | 42.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1991c5c7290f5b3865a924ee1606d5a7cc6cc5d83f25a06ae1f098bbbc13822b
|
|
BLAKE2b-256 checksum How to use checksums |
65066abf3e6edf6445adcfc85c4f90ac0e633e9229c2da8e74645c5047167f5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency log