PromptisePromptise
Why PromptiseUse casesRoadmapDocs
GitHubGet started
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Guides> Prompt Engineering

Program-of-Thought in Python: Run Model Code Safely

Compute answers with model-written code in Python: an output contract, a locked-down Docker sandbox, a two-run check, and when to use a code tool instead.

Level
Advanced
Reading time
8 min
Published
Sep 7, 2025
Last updated
Oct 25, 2026
By
Promptise Team
  • Program-of-Thought (PAL)
  • Code Execution and Sandboxing
  • Tool Calling
Contents · 11 sections↓
  1. 01How do you run model-written code safely to compute an answer?
  2. 02How compute offload works
  3. 03When to run code yourself (and when to use a tool)
  4. 04Step 1: Write a prompt with an output contract
  5. 05Step 2: Run the code in a locked-down container
  6. 06Step 3: Generate twice and compare
  7. 07Step 4: Run it on a real problem
  8. 08Running it in production
  9. 09Common mistakes
  10. 10Frequently asked questions
  11. 11Next steps

Program-of-thought prompting has the model write code and lets an interpreter compute the answer. The prompt is the easy part. The hard part is running code a model wrote without exposing your server, and knowing when the result is wrong. This guide is for Python developers who want to compute answers this way in production. You'll build a small runner that generates code, runs it in a locked-down Docker container, checks it with a second generation, and learn when a hosted code tool is the better choice.

[01]

How do you run model-written code safely to compute an answer?

Ask the model for a program with a strict output contract (one JSON line), run it in an isolated sandbox with no network, a read-only filesystem, a non-root user and CPU, memory and time limits, then parse only the last line. Generate the program twice and accept the answer only if both runs agree. If you don't want to operate a sandbox, use a provider's hosted code-execution tool instead.

Key takeaways:

  • Generated code is untrusted input. Never exec() it in your application process.

  • A container is a sandbox only with the right flags: no network, read-only root, dropped capabilities, resource limits.

  • Kill the container on timeout. Killing the docker client process leaves the container running.

  • Compare two generations. Disagreement is a cheap signal that the setup is wrong.

  • Normalize before comparing. 5952.35 and "5952.35" are the same answer.


[02]

How compute offload works

The model reads the problem and writes the computation. The interpreter does the arithmetic exactly. Your code owns everything around it: the contract, the sandbox, the comparison and what happens on failure. For the background on why this beats asking for the number directly, see what PAL and Program-of-Thought are.

The model writes two programs for the same problem, each runs in a locked-down sandbox and prints one JSON line, and your code normalizes and compares the two answers, accepting agreement and escalating a disagreement or error to a stronger model, more effort or a person.
FigureCompute offload: two generated programs, each run in a sandbox, compared before the answer is accepted

[03]

When to run code yourself (and when to use a tool)

Option

Use it when

Trade-off

Your own sandbox (this guide)

You need a fixed pipeline, your own libraries, data that can't leave your infrastructure, or any model provider

You operate and secure it

Provider code tool, such as OpenAI's code interpreter or Anthropic's code execution tool

Occasional computation inside a chat or agent; you're fine with the provider's container and pricing

Less control over libraries, limits and data location; tied to one provider

A narrow tool (calculator, date function)

The computations are few and known in advance

Safest and simplest, but only covers what you built

No code at all

The task is reading or judgment, or one simple operation

Arithmetic errors on long calculations

If the model only needs to multiply a few numbers, a calculator tool that parses arithmetic is enough. Arbitrary code is worth it when the logic varies: loops, dates, conditions, tables.


[04]

Step 1: Write a prompt with an output contract

The contract tells your code where the answer is. The rules also steer the program toward things the sandbox allows.

Pythoncompute_offload.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PROMPT = """Write a Python 3 program that solves the problem below.
Rules:
- Use only the standard library. No file, network or subprocess access.
- Use decimal.Decimal for money and round to cents only at the end.
- Name each quantity from the problem, and comment each step.
- The last line must print one JSON object: {{"answer": <value>, "unit": "<unit>"}}
Return only the code, no Markdown fences.

Problem: {problem}"""


def generate_code(problem: str) -> str:
    response = client.chat.completions.create(
        model="gpt-5-mini",
        reasoning_effort="minimal",
        messages=[{"role": "user", "content": PROMPT.format(problem=problem)}],
    )
    return response.choices[0].message.content

The "no file, network or subprocess" rule is a request, not a control. The sandbox in step 2 enforces it. Decimal matters for money: floats accumulate rounding errors in cents over many steps.


[05]

Step 2: Run the code in a locked-down container

Pythoncompute_offload.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def run_sandboxed(code: str, timeout: int = 20) -> dict:
    name = f"pot-{uuid.uuid4().hex[:12]}"
    cmd = [
        "docker", "run", "--rm", "-i", "--name", name,
        "--network", "none",                       # no internet, no internal services
        "--memory", "256m", "--cpus", "1", "--pids-limit", "64",
        "--read-only", "--tmpfs", "/tmp:size=16m",  # nothing persists
        "--cap-drop", "ALL", "--security-opt", "no-new-privileges",
        "--user", "65534:65534",                    # run as nobody
        "python:3.11-slim", "python", "-I", "-",
    ]
    try:
        proc = subprocess.run(cmd, input=code, capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        # The timeout kills the docker client, not the container. Kill it too.
        subprocess.run(["docker", "kill", name], capture_output=True)
        return {"error": "timeout"}
    if proc.returncode != 0:
        return {"error": proc.stderr.strip().splitlines()[-1][:300]}
    try:
        return json.loads(proc.stdout.strip().splitlines()[-1])
    except (json.JSONDecodeError, IndexError):
        return {"error": "last line is not JSON", "stdout": proc.stdout[-300:]}

Each flag closes a door:

  • --network none: the code can't call out, exfiltrate data or reach your internal services.

  • --read-only plus a small tmpfs: it can't modify the image or leave files behind.

  • --cap-drop ALL, no-new-privileges, --user 65534: it runs as an unprivileged user with no extra kernel capabilities.

  • --memory, --cpus, --pids-limit: a runaway loop or fork bomb can't starve the host.

  • python -I: isolated mode ignores environment variables and the user site directory.

The program goes in through stdin, so nothing is mounted from your host. Don't pass secrets as environment variables; the code can read them.

The timeout comment is there for a reason. subprocess.run(..., timeout=5) returns after five seconds, but docker ps still shows the infinite-loop container running: the timeout stops the client process only. Naming the container and calling docker kill fixes it.

Test the sandbox with code that misbehaves:

Python
print(run_sandboxed('import urllib.request\nurllib.request.urlopen("https://example.com", timeout=5)'))
print(run_sandboxed('open("/etc/passwd", "a").write("x")'))
print(run_sandboxed("while True: pass", timeout=5))
Output
{'error': 'urllib.error.URLError: <urlopen error [Errno -3] Temporary failure in name resolution>'}
{'error': "PermissionError: [Errno 13] Permission denied: '/etc/passwd'"}
{'error': 'timeout'}

Containers share the host kernel. For code from the public internet or high-value hosts, add a stronger boundary such as gVisor (--runtime runsc) or a microVM. Docker's security overview explains what the defaults do and don't cover.


[06]

Step 3: Generate twice and compare

A program can run cleanly and still be wrong: a misread condition, an off-by-one month. Two independent generations that agree are a useful signal; two that disagree tell you the setup is ambiguous or wrong. When they disagree, don't guess.

Pythoncompute_offload.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def normalize(value) -> str:
    """Treat 5952.35 and "5952.35" as the same answer."""
    try:
        return str(Decimal(str(value)).quantize(Decimal("0.01")))
    except InvalidOperation:
        return str(value).strip().lower()


def solve(problem: str, tries: int = 2) -> dict:
    runs = [run_sandboxed(generate_code(problem)) for _ in range(tries)]
    if any("answer" not in r for r in runs):
        return {"status": "error", "runs": runs}
    if len({normalize(r["answer"]) for r in runs}) == 1:
        return {"status": "ok", **runs[0]}
    return {"status": "disagree", "runs": runs}

Without the normalize step, the two programs below are reported as a disagreement: one printed 5952.35 as a number, the other "5952.35" as a string. Both were right.

Output
{'status': 'disagree_or_error', 'runs': [{'answer': 5952.35, 'unit': 'dollars'}, {'answer': '5952.35', 'unit': 'dollars'}]}

For higher stakes, sample three programs and take the majority, as in the self-consistency extension of Program of Thoughts (Chen et al., 2022). On disagreement, escalate: a stronger model, more reasoning effort, or a person.


[07]

Step 4: Run it on a real problem

Pythoncompute_offload.py
print(solve(
    "A savings account starts at $2,500. Each month it earns 0.35% interest on the balance, "
    "then the owner deposits $180. After the 3rd, 9th and 15th deposit the owner also "
    "withdraws $400. What is the balance after 24 months, to the cent?"
))
Output
{'status': 'ok', 'answer': '5952.35', 'unit': 'dollars'}

That is correct. Asked the same question directly at minimal reasoning, the model answered 11611.38. One of the generated programs (trimmed):

Output
initial_balance = Decimal('2500')            # starting balance in dollars
monthly_interest_rate = Decimal('0.35') / Decimal('100')  # monthly interest rate as decimal (0.35%)
…
for month in range(1, months_total + 1):
    interest = balance * monthly_interest_rate
    balance += interest  # add interest to balance
    deposit_number = month  # deposit count equals month number
    balance += regular_deposit
    if deposit_number in withdrawal_after_deposit_numbers:
        balance -= special_withdrawal
…
print(json.dumps(output))

You can audit the setup in a few lines: interest before the deposit, withdrawals after deposits 3, 9 and 15. That's the other benefit of code: the reasoning is inspectable.


[08]

Running it in production

Keep sandboxes warm

Starting a container per question adds startup latency to every answer. For high volume, keep a pool of pre-started sandboxes and reset them between jobs, or use a managed sandbox service.

Log the program, not only the answer

Store the generated code, the sandbox result and the status. When a user disputes a number, the program shows exactly what was computed.

Use a framework sandbox if you already run agents

If your agent framework provides a sandbox, use it rather than building one. In Promptise Foundry, for example, build_agent(..., sandbox=True) runs agent code in a Docker container with the network off and as an unprivileged user by default, and refuses to start if Docker isn't available rather than running unsandboxed.


[09]

Common mistakes

Mistake

Why it fails

Fix

exec() in your app process

The code can read env vars, files and the network

Run in a separate container or VM

Default docker run

Network on, writable root, root user

Add the flags from step 2

Timeout on subprocess.run only

The container keeps running

Name the container and docker kill it

Secrets in the sandbox environment

Generated code can print them

Pass only the problem through stdin

Comparing raw JSON values

Types differ between runs

Normalize, for example to Decimal

Answer parsed from anywhere in stdout

Debug prints break parsing

Contract: the last line is one JSON object


[10]

Frequently asked questions

Is a Docker container enough isolation for LLM-generated code?

For code that only you trigger, with the flags above, it's a reasonable baseline. Containers share the host kernel, so for untrusted users or sensitive hosts add gVisor or a microVM, and keep the sandbox host away from production secrets.

Should I use a hosted code interpreter instead?

If you already use one provider and the computation is occasional, yes: it's less to operate. Run your own sandbox when you need your own libraries, data that must stay on your infrastructure, or a provider-neutral pipeline.

Why not restrict imports with an AST check instead of a sandbox?

Import filters are easy to bypass in Python (through __builtins__, getattr chains and similar). They're useful to reject obviously wrong code early, but they are not a security boundary.

How many generations should I compare?

Two catches many setup errors cheaply. Three lets you take a majority vote. Measure on your own questions before adding more.

What if the model imports a library the sandbox doesn't have?

The run fails with ModuleNotFoundError, which your runner returns as an error. List the available libraries in the prompt, and build the sandbox image with them preinstalled.


[11]

Next steps

  • The concept behind this guide: what PAL and Program-of-Thought prompting are.

  • Sandboxing for agents that write code, with escape attempts: AI agent code execution sandbox.

  • Agents that act through code: code-action agents in Python.

  • Voting over several answers: self-consistency and least-to-most.

  • Checking outputs before you use them: LLM output validation.

Contents

  1. 01How do you run model-written code safely to compute an answer?
  2. 02How compute offload works
  3. 03When to run code yourself (and when to use a tool)
  4. 04Step 1: Write a prompt with an output contract
  5. 05Step 2: Run the code in a locked-down container
  6. 06Step 3: Generate twice and compare
  7. 07Step 4: Run it on a real problem
  8. 08Running it in production
  9. 09Common mistakes
  10. 10Frequently asked questions
  11. 11Next steps

Learning paths

Want more structure? Paths put guides in order, like a short course.

See the paths →

> Read next

Related guides

  • // 01Beginner · 4 minWhy Can't LLMs Count Letters? Tokens and ProbabilityWhy LLMs miscount letters, reverse strings wrong and miss word limits: tokenization, sampling, real failures, and why exact work belongs in your code.
  • // 02Intermediate · 8 minReAct Agent From Scratch in Python: The Reason-Act LoopBuild a ReAct agent from scratch in Python: the Thought, Action, Observation loop, a 60-line controller with a real trace, and how it maps to tool calling.
  • // 03Intermediate · 7 minWhat Are PAL and Program-of-Thought Prompting?PAL and Program-of-Thought prompting explained: the model writes code, an interpreter computes the answer. How they differ, when to use them, real example.
  • // 04Advanced · 8 minLLM Tool Call Planning: Validate Plans Before ExecutingHave the LLM plan multi-step API calls as JSON, validate tools, arguments and guards in code, then execute safely. Python code and a real plan that failed.
  • // 05Intermediate · 4 minHow LLM Tool Calling Works: The Model Never Runs ToolsHow LLM tool calling works: the model writes a request, your code runs it. A real two-step refund exchange and design rules for tools, results and gates.
  • // 06Intermediate · 18 minStop Secrets Leaking Into LLM Logs and TracesHow API keys and tokens end up in LLM logs and traces, and how to redact them from tool results, observability and log lines before anything is stored.

Keep going.

Browse every guide by topic and level, or follow a learning path that puts them in order.

All guidesLearning paths