Program of thoughts (PoT): a complete guide
Language models are great at reasoning but bad at arithmetic. Chain-of-thought makes the model do both in its head, so a single botched multiplication sinks the whole answer. Program of thoughts (PoT) flips that: the model writes the reasoning as a Python program and hands the actual math to an interpreter. On eight math and finance benchmarks it beat chain-of-thought by roughly 12% on average (Chen et al., 2022).
See it work
Ask a model for the 50th Fibonacci number. Chain-of-thought tries to track the running sum in prose, and it almost always drifts somewhere around the tenth term.
Prompt (CoT):
Q: What is the 50th Fibonacci number (starting 0, 1)? Let's think step by step.
Output:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... (the model keeps adding by hand)
... I think it's around 12586269025? (often wrong by a few terms)
Now ask the same question, but tell the model to write a program. It stops doing mental math and describes the procedure instead.
Prompt (PoT):
Q: What is the 50th Fibonacci number (starting 0, 1)?
# Write Python. Store the result in `ans`.
Output:
fib = [0, 1]
for i in range(2, 50):
fib.append(fib[i-1] + fib[i-2])
ans = fib[49] # 7778742049
The interpreter runs the loop and returns 7778742049 — exact, every time. The model never had to be good at addition; it only had to be good at describing addition.
The mental model
Think of a smart colleague with a calculator. You wouldn't ask them to multiply six-digit numbers in their head — you'd ask them to set up the equation and let the calculator finish. PoT splits the job along the same seam.
Reasoning is the model's job. Computation is the computer's job. Don't make one do the other.
Chain-of-thought blurs that line: the model reasons and calculates in the same token stream, where it's strong at the first and shaky at the second. PoT keeps the model in its lane.
How it works
- Prompt for code, not prose. Give the model the question and ask it to express the solution as a program (Python by default), storing the result in a variable like
ans. - Bind variables to meaning. The model names variables after the quantities they represent —
interest_rate,num_months— instead ofxandy. This semantic binding is what keeps the generated code aligned with the word problem. - Offload the math. The model writes the relationships; it does not evaluate them. Multiplication, iteration, equation-solving (via libraries like SymPy) all go to the interpreter.
- Execute and read off the answer. A Python interpreter runs the program and returns the value of
ans. That value is the answer — no second LLM pass needed.
Why it works
| Factor | Why it matters |
|---|---|
| No mental arithmetic | The model offloads the step it's worst at, so calculation errors drop to near zero. |
| Semantic variable names | Meaningful names anchor each line to the problem, reducing logic slips. |
| Deterministic execution | The same program always yields the same number — no token-by-token drift. |
| Iteration and recursion | Loops and library calls handle steps prose can't track, like 50 Fibonacci terms. |
| Separation of concerns | Reasoning errors and computation errors become independent and debuggable. |
Where it shines
PoT pays off whenever the answer hinges on exact numbers rather than fluent text.
- Math word problems — arithmetic, algebra, and multi-step quantitative reasoning.
- Financial QA — multi-row table math, percentages, and ratios where CoT slips most.
- Tabular reasoning — pulling values from a table and combining them.
- Any task with a clean numeric answer that a program can compute and verify.
The headline numbers, all using Codex (code-davinci-002) in the original paper:
| Dataset | CoT | PoT |
|---|---|---|
| GSM8K | 63.1% | 71.6% |
| AQuA | 45.3% | 54.1% |
| SVAMP | 76.4% | 85.2% |
| TabMWP | 65.2% | 73.2% |
| FinQA | 40.4% | 64.5% |
| ConvFinQA | 45.6% | 64.6% |
| TATQA | 61.4% | 69.0% |
Finance is the standout: FinQA jumps over 24 points and ConvFinQA 19, because those tasks stack many small calculations where CoT compounds rounding and slips. Layering self-consistency on top (PoT-SC) pushes GSM8K to 80.0% and FinQA to 68.1%. PoT also wins in zero-shot: GSM8K climbs from 40.5% (CoT) to 57.0%, and MultiArith from 79.3% to 92.2%.
When to use it (and when not)
Reach for PoT when:
- The answer is a number, table, or anything a program can compute exactly.
- The problem needs multi-step arithmetic, iteration, or equation-solving.
- You can run the generated code in a sandbox and trust the interpreter.
Skip it when:
- The task is open-ended language — summaries, classification, creative writing. There's nothing to compute.
- Questions are wildly diverse with no representative exemplars; PoT only reached 54% on AQuA's varied algebra, its weakest result.
- You can't safely execute generated code (see the security note below).
Cost and latency. PoT adds a code-execution round trip on top of generation, and PoT-SC samples several programs and votes — multiplying both token cost and runtime. Use single-sample PoT first; reserve self-consistency for the hardest problems.
Model fit. PoT needs a model fluent in code. Code-tuned or modern general models (Codex, GPT-4-class, Claude) handle it; weak or tiny models that can't write correct Python will fail at the new task instead of the old one.
Escalation. Start with CoT. If accuracy stalls on quantitative tasks because of arithmetic errors, switch to PoT. If a few programs still disagree, add self-consistency voting over PoT samples.
| Variant / alternative | When to choose it |
|---|---|
| PoT (single sample) | Default for numeric tasks; one program, one execution. |
| PoT + self-consistency | Hardest problems; sample N programs, run each, vote on ans. |
| PAL (program-aided LMs) | Near-identical idea from the same period; interleaves natural-language comments with code. Also strong on symbolic/algorithmic tasks. |
| Chain-of-thought (CoT) | Reasoning-heavy tasks with little or no computation. |
| Faithful CoT | When you want the natural-language plan and the executable code to stay provably aligned. |
Structure and components
A PoT prompt has three moving parts:
- The question — the problem statement, often with a table or context.
- A code instruction — "write Python," "store the answer in
ans," sometimes a hint to use SymPy for symbolic work. - Few-shot exemplars (optional) — a handful of question-to-program pairs that demonstrate the style: meaningful variable names, no inline answer, result assigned to
ans.
A standard exemplar looks like this:
Q: Carla downloads a 200 GB file at 2 GB/min, but 40% in, the
download restarts. How long total, in minutes?
# Answer in Python, store result in `ans`.
total_gb = 200
rate = 2
gb_before_restart = total_gb * 0.40
time_before = gb_before_restart / rate
time_full = total_gb / rate
ans = time_before + time_full
Note what's not there: no "the answer is 220 minutes" line. The program is the reasoning, and the interpreter produces the number.
The core mechanism
The loop around the model is small. Generate a program, execute it, return ans; resample if it crashes.
import subprocess, textwrap
def pot_solve(question, generate, max_tries=3):
for _ in range(max_tries):
program = generate(POT_PROMPT + question) # LLM writes Python
try:
scope = {}
exec(program, {"__builtins__": SAFE_BUILTINS}, scope)
return scope["ans"] # interpreter computes
except Exception:
continue # resample on error
return None
For self-consistency, run pot_solve N times and return the most common ans. Because execution is deterministic, disagreement comes from the programs, not the math — exactly the signal you want to vote on.
Configuration
| Parameter | Guideline |
|---|---|
| Temperature | 0 for single-sample PoT (deterministic reasoning). 0.4–0.7 when sampling for self-consistency. |
| Samples (N) | 1 by default; 5–40 for PoT-SC, trading cost for accuracy. |
| Stop sequence | End generation at the start of a new question, so the model writes one program. |
| Result variable | Standardize on ans (or similar) so parsing the output is trivial. |
| Allowed imports | Whitelist math, NumPy, SymPy; block file, network, and OS access. |
Implementation workflow
- Write 2–8 few-shot exemplars in your domain: question, code instruction, program ending in
ans. - Set temperature to 0 and a stop sequence so the model emits exactly one program.
- Parse the generated code and run it in a sandboxed interpreter with a tight import whitelist.
- Read
ans; on a crash or missing variable, resample (and log the failing program). - If accuracy plateaus, add self-consistency: sample several programs and vote.
Do and don't
- Do name variables after real quantities — semantic binding is half the technique.
- Do sandbox execution; you're running model-written code.
- Do keep exemplars in the same style and domain as your real questions.
- Don't ask the model for the final number — let the interpreter compute it.
- Don't use PoT for tasks with no computable answer.
- Don't run generated code with unrestricted system access.
Debugging
| Symptom | Likely cause | Fix |
|---|---|---|
| Code won't run | Syntax error or undefined name | Resample; tighten exemplars; lower temperature. |
ans missing | Model wrote prose or forgot the variable | Reinforce "store result in ans" in the instruction and every exemplar. |
| Right code, wrong setup | Reasoning slip, not a math error | Improve exemplars; the bug is in variable logic, not the interpreter. |
| Inconsistent answers | Hard problem, ambiguous wording | Add self-consistency voting over several programs. |
| Crashes on imports | Library outside the whitelist | Either add it deliberately or steer exemplars away from it. |
Testing and how to prove it
Run PoT and CoT on the same held-out set and compare exact-match accuracy. Because PoT execution is deterministic, you can also separate reasoning errors (wrong program) from computation errors (none — the interpreter is exact), which CoT can't do.
def compare(dataset, cot_solve, pot_solve):
cot = sum(cot_solve(q) == a for q, a in dataset)
pot = sum(pot_solve(q) == a for q, a in dataset)
n = len(dataset)
return {"CoT": cot / n, "PoT": pot / n}
If PoT doesn't beat CoT on a quantitative set, your exemplars or sandbox are likely the problem — the technique itself reliably wins on numeric tasks.
Limitations
- Only as good as the program. PoT removes arithmetic errors but not reasoning errors; a wrongly-set-up equation still gives a wrong answer.
- Needs a computable answer. Tasks without a numeric or programmatic target gain nothing.
- Diverse problem sets hurt. Without representative exemplars, accuracy drops — AQuA's varied algebra capped PoT at about 54%.
- Requires a code-fluent model. Models that can't write correct Python fail outright.
- Execution overhead. Running code (and voting, for PoT-SC) adds latency and cost.
Risk and ethics
You are executing model-generated code. The PoT authors flag that generated programs "could contain certain dangerous or risky code snippets." Always run in a sandbox with a strict import whitelist (math, NumPy, SymPy) and no file, network, or shell access. Untrusted input plus code execution is a classic injection surface.
Ecosystem
PoT sits in the "reasoning-as-code" family alongside its close sibling PAL.
| Technique | Reasoning carrier | Computation | Best at |
|---|---|---|---|
| CoT | Natural language | In the model (error-prone) | General reasoning |
| PoT | Python program | External interpreter | Numeric, financial, tabular |
| PAL | Code + NL comments | External interpreter | Numeric, symbolic, algorithmic |
PoT composes well with self-consistency (vote over programs) and slots into agents and RAG pipelines as the "do the math reliably" step: retrieve the numbers, then let PoT compute on them. Tools like LangChain and DSPy ship code-execution and Python-REPL components that wire up the generate-execute loop.
Real-world payoff. On FinQA — multi-step questions over financial tables — PoT lifted accuracy from 40.4% to 64.5%, a 24-point gain (Chen et al., 2022). When an answer is a chain of small calculations, moving the math out of the model's head is the single biggest win available.
Summary
- PoT separates reasoning from computation: the model writes a program, an interpreter runs it.
- It beats chain-of-thought by about 12% on average across eight math and finance benchmarks.
- Semantic variable names anchor the code to the problem and cut logic slips.
- It shines on math word problems, financial QA, and tabular reasoning — anything with an exact numeric answer.
- Skip it for open-ended language tasks or when you can't safely execute code.
- Always sandbox the generated program; you're running model-written code.
- Start with single-sample PoT; add self-consistency voting only for the hardest problems.
Read Next
Start reading to get personalized recommendations
Explore Unread
Great job! You've read all available articles