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_errorto 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 needsOPENAI_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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87951b9d927603996164b2b50d073b5d2b46535bf12a8a65e803ef1d09456b8d
|
|
| MD5 |
90741a0e0e609213c62647bcec9495d9
|
|
| BLAKE2b-256 |
412836ca73227764df39abbaccce7a05eb278f27f4cc375c997618d6ef422ccf
|
Provenance
The following attestation bundles were made for schema2code-0.2.0.tar.gz:
Publisher:
publish.yml on DW-dev-UE/schema2code
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
schema2code-0.2.0.tar.gz -
Subject digest:
87951b9d927603996164b2b50d073b5d2b46535bf12a8a65e803ef1d09456b8d - Sigstore transparency entry: 2269635988
- Sigstore integration time:
-
Permalink:
DW-dev-UE/schema2code@81c85770f96dd8b2d7d0ecd836905da176e6b93d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/DW-dev-UE
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@81c85770f96dd8b2d7d0ecd836905da176e6b93d -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1991c5c7290f5b3865a924ee1606d5a7cc6cc5d83f25a06ae1f098bbbc13822b
|
|
| MD5 |
da2de3d81e4b6a03472b3e5c38129495
|
|
| BLAKE2b-256 |
65066abf3e6edf6445adcfc85c4f90ac0e633e9229c2da8e74645c5047167f5b
|
Provenance
The following attestation bundles were made for schema2code-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on DW-dev-UE/schema2code
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
schema2code-0.2.0-py3-none-any.whl -
Subject digest:
1991c5c7290f5b3865a924ee1606d5a7cc6cc5d83f25a06ae1f098bbbc13822b - Sigstore transparency entry: 2269636410
- Sigstore integration time:
-
Permalink:
DW-dev-UE/schema2code@81c85770f96dd8b2d7d0ecd836905da176e6b93d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/DW-dev-UE
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@81c85770f96dd8b2d7d0ecd836905da176e6b93d -
Trigger Event:
release
-
Statement type: