Mistral can reason about code but cannot run it. Five concepts — why agents need to execute code, the three sandbox options, the security model, the execute_python tool, and the self-debugging loop that turns a chatbot into a reliable computing engine.
The 5 concepts of code execution
Tap any concept to jump to its 5-card cluster.
The model reasons; the interpreter computes
A language model predicts text — it doesn't calculate. Ask it for 3.7 ** 12 and it produces a plausible-looking number that may be subtly wrong, with no error to warn you.
A code execution tool flips the job: the model writes Python, a sandbox runs it, and you capture stdout, stderr, and the exit code. The model authors and reasons; the interpreter delivers the deterministic answer.
The mathematician with a calculator
BEFORE: Imagine asking a brilliant mathematician to compute 3.7 to the 12th power entirely in their head, then trusting the number they say out loud.
PAIN: They round at each step, carry a mental slip, and confidently hand you an answer that's slightly off — and you don't notice until you check it against a calculator.
MAPPING: A sandbox is that calculator. The mathematician (the model) still decides what to compute and how, but instead of guessing the result, they type the expression and read back exactly what the machine returns. The interpreter is always right.
One round trip through the tool
- Model calls the toolIt emits a tool call named
execute_pythonwith a code string as the argument. - Agent runs the codeYour framework hands the code to a local executor — a subprocess or a container.
- Capture the outcomeCollect
stdout,stderr, and the exit code (0 means success). - Return as a tool messageFeed those results back so the model can turn them into its final answer.
Pseudocode
# WRONG: trust the model's mental math answer = model.ask("what is 3.7 ** 12?") # -> a confident, maybe-wrong number # RIGHT: model writes code, machine runs it call = model.wants_tool() IF call.name == "execute_python": result = executor.run(call.code) # result = stdout, stderr, exit_code send_back_to_model(result) # the interpreter produced the number, # not the language model.
Misconceptions
Give the agent a sandbox so it computes with an interpreter instead of guessing. The model authors code; the machine returns the truth.
Reserve the model for reasoning and code authoring — deterministic work belongs to the executor.
Three sandboxes, three trade-offs
There is no single "right" sandbox. You choose along three axes: security, cold-start speed, and infrastructure complexity. Pushing one usually costs another.
The three practical options are a subprocess (a child process, fastest but low isolation), a Docker container (ephemeral and isolated, the production default), and E2B cloud (a managed sandbox VM, no Docker needed, works on any OS).
Where you let a stranger cook
BEFORE: A stranger needs to cook one dish. You have to decide which kitchen to hand them.
PAIN: Your own kitchen (subprocess) is instant and easy, but they can rummage through every drawer and torch the house. Too trusting for a stranger.
MAPPING: A rented, sealed test kitchen you tear down after (Docker container) is safe and repeatable, but takes a minute to set up. A commercial kitchen-for-hire in another city (E2B cloud) needs zero setup on your end and works from anywhere — you just ship the recipe and get the plate back. Same meal, very different blast radius.
The three options
- subprocessA child process, cold-start under 50ms, low complexity. But it shares your filesystem and network — low-to-medium security. Trusted input only.
- Docker containerHigh security via OS-level isolation; 1–3s cold start; medium complexity. Destroyed after each run. The production choice for untrusted input.
- E2B cloudVery high security, ~2s start, low complexity, no Docker required. Runs in a managed cloud VM; free tier ~100 runs/hour.
Which sandbox should I use?
IF code is trusted (your own model, dev/prototyping, tight prompt): → subprocess # fastest, simplest IF input is untrusted / user-facing and you run in production: → Docker container # isolate it IF no Docker available (Windows, CI, serverless) or you want it managed: → E2B cloud sandbox # default when unsure: Docker
All three can hide behind one identical executor interface, so swapping is a one-line change.
Misconceptions
~/.ssh/id_rsa or open sockets. Isolation requires Docker or a cloud VM, not just a separate process.Choose the sandbox by trust level: subprocess for trusted dev, Docker for production, E2B when Docker isn't an option.
Hide all three behind one run(code) -> result interface so you can swap them with a single line.
Assume the code is hostile
Model-generated code can be jailbroken or simply wrong, so you defend against five real attacks: path traversal, infinite loops, fork bombs, network exfiltration, and filesystem damage.
A single SecurityPolicy object encapsulates every limit — timeout, memory cap, CPU share, no network, read-only filesystem — and the same policy configures whichever executor you use. Defense is layered, not a single wall.
A bank teller behind glass
BEFORE: Imagine a bank that lets any visitor walk straight into the vault to "help themselves" to the service they came for.
PAIN: Most visitors are fine, but one bad actor can empty the vault, and a confused one can knock everything over. Trusting good intentions is not a control.
MAPPING: A SecurityPolicy is the teller counter: bulletproof glass (read-only filesystem), a locked cash drawer (no network to exfiltrate), a time limit per customer (timeout), and a queue cap (CPU/memory limits). The visitor gets exactly the service, and nothing else — no matter their intent.
The layers, attack by attack
- timeout + CPU limitKills infinite loops and starves fork bombs of CPU before they hang the host.
- memory capAn OOM kill stops memory bombs from crashing your agent process.
- --network=noneBlocks exfiltration — code can't POST your secrets to
attacker.com. - read-only filesystemOnly a mounted
/workspaceis writable, so path traversal andrm -rf /find nothing to damage. - ephemeral containerThe
--rmflag destroys everything the run created on exit.
Pseudocode
SecurityPolicy: timeout_seconds = 10 # kill hangs max_memory = 128 MB # OOM guard max_cpus = 0.5 # half a core network_disabled = true # no exfil readonly_fs = true # no writes image = "python:3.12-alpine" # one policy, passed to ANY executor: executor = DockerExecutor(policy) executor.run(untrusted_code)
Misconceptions
--network=none won't stop an infinite loop. You need the layers together.Treat every execution as untrusted and stack layered limits in a single SecurityPolicy. Timeout, memory, CPU, no network, read-only FS, ephemeral container.
The prompt is guidance; the runtime is the guarantee.
One tool schema, any backend
The model sees a single tool definition — execute_python with a code parameter — no matter which sandbox runs it. The schema's description is what the model reads to decide when to call it, so it must be concrete: say it returns stdout, stderr, and an exit code.
Behind that schema sit interchangeable executors (subprocess or Docker) that share one run(code, timeout) signature and return one ExecResult. Swap backends without touching the agent.
A standard wall socket
BEFORE: Imagine every appliance needed a differently-shaped plug wired directly to a specific power source.
PAIN: Change the power source and you rewire every appliance. Nothing is swappable; each device is welded to one supply.
MAPPING: The tool schema is a standard wall socket. The model (appliance) always plugs into the same shape — execute_python(code) — while behind the wall the supply can be a subprocess, Docker, or E2B. Change the backend and nothing on the model's side has to change.
From schema to result
- Define the schemaName it
execute_python; thecodestring is required,timeout_secondsoptional. Describe what comes back. - Model emits a tool callIt fills in the code argument as valid JSON in its response.
- Write code to a temp fileThe executor writes it out; never run with a shell, to avoid injection.
- Run and captureLaunch subprocess or container, collect stdout/stderr/exit code into one ExecResult.
- Format for the modelTurn ExecResult into a compact tool message the model can read next turn.
Pseudocode
TOOL "execute_python": param code : string (required) param timeout : int (default 10) description : "runs Python, returns stdout, stderr, exit_code" FUNCTION run(code, timeout): path = write_temp_file(code) TRY: out, err, exit = launch(path, timeout) # shell = OFF (no injection) CATCH timeout: RETURN result(err="timed out", exit=124) FINALLY: delete_temp_file(path) RETURN result(out, err, exit)
Same signature whether the backend is a subprocess or a Docker container.
Misconceptions
Expose one clear execute_python schema and back it with interchangeable executors sharing a single result shape.
Concrete descriptions drive tool selection; a temp file plus shell-off launch keeps execution safe.
Feed the error back and let it fix itself
When the executor returns a non-zero exit code, you don't give up — you append the stdout and stderr to the message history and call the model again. It reads its own error, diagnoses the bug, and rewrites the code.
This is a ReAct loop for code: write → run → read error → fix → retry, capped at a few attempts (typically 3). The retry counter lives in your code; the model just sees a fresh error each round.
A student with the answer key's red marks
BEFORE: Picture a student who submits homework and simply never learns whether any answer was wrong.
PAIN: Without feedback they repeat the same mistake forever — no error message, no correction, no improvement.
MAPPING: The self-debugging loop hands the graded paper back with the red marks (the stderr). The student (model) reads exactly what failed — "NameError: df not defined" — corrects that specific line, and resubmits. A cap of three tries stops an endless back-and-forth.
One pass of the loop
- Write & callThe model generates code and calls
execute_python. - RunThe executor runs it and returns stdout, stderr, and the exit code.
- Branch on exit codeIf non-zero, append the error as a tool message and ask the model to retry.
- Retry, cappedRepeat up to 3 attempts, then return the last error to the caller.
- SuccessOn exit code 0, return the final stdout as the agent's answer.
Pseudocode
messages = [ system, user_request ] FOR attempt IN 1..max_retries: reply = model.call(messages, tools) IF no tool_call: RETURN reply.text messages.append(reply) # FIRST result = executor.run(reply.code) messages.append(tool_result) # THEN IF result.exit_code == 0: CONTINUE # let model summarize IF attempt == max_retries: RETURN "failed: " + result.stderr
Misconceptions
On failure, append the error to the history and let the model read, fix, and retry — capped at ~3 attempts.
Append the assistant tool-call message before the tool result; keep the retry counter in your code, not the model.
Test the core insight
One question per concept. Tap to reveal the answer.
execute_python tool description spell out its return value?Open the full module on desktop
The desktop version has you build both SubprocessExecutor and DockerExecutor against a shared SecurityPolicy, wire up the full CodeExecutionAgent self-debugging loop, and run real pandas code inside a locked-down Docker container — in Python and Node.
Module 8 of 24 · Open Source Track · Tool Use