Program-aided language models (PAL): a complete guide
Language models are great at reading a word problem and terrible at doing the arithmetic. PAL keeps the reading and throws away the arithmetic: the model writes a short program for the reasoning steps, then a Python interpreter runs it and returns the exact answer. With this one move, Gao et al. (2022) pushed GSM8K from 65.6% (chain-of-thought) to 72.0% top-1, beating PaLM-540B by an absolute 15 points with a far smaller model.
See it work
Watch chain-of-thought reason correctly and still get the number wrong, because it's doing the math in its head.
Problem: A bookstore starts the month with 156 books. Each week it sells 23
and restocks 41. After 7 weeks, how many books are on the shelf?
Chain-of-thought:
Each week the net change is 41 - 23 = 18 more books.
Over 7 weeks that's 18 * 7 = 116 books. <-- wrong, 18 * 7 = 126
So 156 + 116 = 272 books.
Answer: 272 <-- wrong
The logic is perfect. The model just fumbled 18 * 7. PAL writes the same logic as code and never touches the calculator itself:
Problem: A bookstore starts the month with 156 books. Each week it sells 23
and restocks 41. After 7 weeks, how many books are on the shelf?
PAL (model generates this program):
books = 156
for week in range(7):
books = books - 23 + 41
answer = books # interpreter computes 282
The interpreter returns 282. Same reasoning, but the arithmetic is offloaded to a tool that never makes a careless mistake.
The mental model
Think of PAL as the difference between an engineer doing long division on a napkin and the same engineer typing it into a calculator. The thinking is theirs; the calculation isn't.
PAL lets the model do what it's good at (turning words into structure) and stops it from doing what it's bad at (executing the math).
Chain-of-thought asks one system to both reason and compute. PAL splits the job: the language model is the planner, the Python interpreter is the solver.
How it works
- Prompt with code exemplars. You few-shot the model with examples that solve problems by writing code, not prose. Comments carry the reasoning; assignments carry the values.
- Generate a program. Given a new problem, the model emits a short Python snippet that ends with an
answervariable. It never states the final number itself. - Execute in an interpreter. A Python runtime runs the snippet. This is where the actual computation happens, deterministically.
- Return the result. The value of
answer(or stdout) is the response. Optionally sample several programs and take a majority vote.
Why it works
PAL's gains come from removing computation from the model's responsibilities. Ranked by how much each factor moves the needle:
| Factor | Why it matters | Evidence |
|---|---|---|
| Deterministic execution | The interpreter never miscalculates; CoT's arithmetic slips vanish | GSM8K 65.6% to 72.0% |
| Robustness to large numbers | Code handles big/awkward values that break mental math | GSM-Hard 23.1% to 61.2% |
| Explicit state via variables | Named variables track multi-step quantities without drift | Object counting 73.0% to 96.7% |
| Structure over prose | Code forces an unambiguous, checkable plan | Repeat copy 68.8% to 90.6% |
Where it shines
PAL wins anywhere the reasoning is tractable but the execution is error-prone. From the paper's 13 tasks across math, symbolic, and algorithmic reasoning (all with Codex / code-davinci-002):
- Math word problems. SVAMP 74.8% to 79.4%, ASDIV 76.9% to 79.6%, and the MAWPS suite near-saturated: SingleEq 96.1%, AddSub 92.5%, MultiArith 99.2%.
- Large-number robustness. On GSM-Hard, which swaps in big numbers, CoT collapses to 23.1% while PAL holds 61.2%. This gap is the clearest demonstration of the technique's value.
- Symbolic reasoning. Colored objects 86.3% to 95.1%, Penguins-in-a-table 79.2% to 93.3%, date understanding 64.8% to 76.2%.
- Counting and copying. Object counting jumps 73.0% to 96.7%; repeat-copy 68.8% to 90.6% — tasks where keeping exact state in prose is brutal.
With self-consistency style sampling (majority@40), PAL reaches 80.4% on GSM8K, edging out Minerva-540B's 78.5% despite using a much smaller base model.
When to use it (and when not)
Reach for PAL when:
- The problem ends in a number, date, count, or table lookup.
- Reasoning is multi-step and arithmetic-heavy, where CoT slips.
- Inputs include large or messy numbers that break mental math.
- You can sandbox a Python interpreter in your stack.
Skip PAL when:
- The task is open-ended generation, summarization, or judgment with no computable answer.
- You can't safely execute model-generated code.
- The base model is weak at code — small non-code models write broken programs.
Cost note. PAL adds an execution hop, not just tokens. Generated programs are usually shorter than verbose CoT chains, so token cost is comparable or lower, but you now own a sandbox: a Python runtime with a timeout, memory cap, and no network. That infra is the real cost, not the API bill.
Model fit. PAL needs a model fluent in code. The original work used Codex (code-davinci-002); any strong modern code-capable model works. Tiny models that can't write valid Python will fail at the generation step before the interpreter ever runs.
Escalate to sampling when greedy decoding plateaus: draw N programs, execute each, and majority-vote the answers (this is what lifts GSM8K from 72.0% to 80.4%).
| Technique | Reasoning lives in | Computation done by | Best for |
|---|---|---|---|
| Chain-of-thought | Prose | The model (error-prone) | General reasoning, no exact answer |
| PAL | Code (full program) | Python interpreter | Arithmetic, counting, dates |
| Program of Thoughts (PoT) | Code + prose interleaved | Python interpreter | Math with running commentary |
| Tool use / function calling | Prose plan + API calls | External tools | Lookups, search, structured APIs |
The PAL program structure
A PAL prompt is a few-shot prompt where every exemplar solves by coding. Three parts do the work:
- The reasoning comments (
# ...) — these replace CoT's prose and steer the model's plan. - The variable assignments — named quantities that hold intermediate state.
- The
answerconvention — a fixed final variable (orprint) so your harness knows what to extract.
# Few-shot exemplar (one of several in the prompt)
Q: Olivia has $23. She buys five bagels for $3 each. How much money left?
# solution in Python:
def solution():
money_initial = 23
bagels = 5
bagel_cost = 3
money_spent = bagels * bagel_cost
money_left = money_initial - money_spent
answer = money_left
return answer
The model mimics this format on the new question. Keep exemplars in the same domain as your task; the comment style is what transfers.
The core mechanism
The harness is small: prompt, generate, execute, extract. The whole technique is the loop below.
import io, contextlib
def run_pal(llm, question, few_shot_prompt):
# 1. Ask the model for a program, not an answer.
program = llm.generate(few_shot_prompt + f"\nQ: {question}\n# solution in Python:\n")
# 2. Execute the generated code in a restricted namespace.
namespace = {}
with contextlib.redirect_stdout(io.StringIO()):
exec(program, namespace) # sandbox this in production
# 3. Extract the agreed-upon result.
if "solution" in namespace:
return namespace["solution"]()
return namespace.get("answer")
For a real platform call the only change is the generation line. Here's a minimal version using a chat completion:
from openai import OpenAI
client = OpenAI()
def generate_program(question, few_shot_prompt):
resp = client.chat.completions.create(
model="gpt-4.1",
temperature=0,
stop=["\nQ:"],
messages=[{"role": "user",
"content": few_shot_prompt + f"\nQ: {question}\n# solution in Python:\n"}],
)
return resp.choices[0].message.content
Never exec model output on a bare interpreter. Generated code is untrusted. Run it in a sandbox (subprocess with a seccomp profile, a container, or a hosted code-execution tool) with no network, a CPU/wall-clock timeout, and a memory cap. A malicious or buggy program should be able to crash, not exfiltrate.
Configuration
| Parameter | Typical value | Why |
|---|---|---|
temperature | 0 for greedy; 0.7 for sampling | Greedy for one shot; sample when majority-voting |
stop | "\nQ:" | Cut generation before the model invents the next question |
| Exemplars | 3–8 code-solution pairs | Enough to fix the format and domain |
| Execution timeout | 1–5 seconds | Kill infinite loops from bad programs |
| Samples (N) | 1, or 40 for majority vote | N=40 is what the paper used to reach 80.4% |
Implementation workflow
- Pick the answer convention. Decide on
answer = ...ordef solution(): return ...and use it in every exemplar. - Write 3–8 exemplars that solve representative problems by coding, with reasoning in comments.
- Stand up a sandbox for execution before you wire in the model.
- Generate, execute, extract with the loop above.
- Add sampling only if greedy accuracy plateaus.
- Log failures by category — generation errors vs runtime errors vs wrong answers — to know what to fix.
Do
- Put the reasoning in code comments; that's where CoT's value moves.
- Keep exemplars in your task's domain.
- Set a hard execution timeout and catch exceptions as wrong-answers.
Don't
- Let the model print the final number in prose — make it return a variable.
- Run untrusted code unsandboxed.
- Use a model that's weak at code and expect valid programs.
Debugging decision tree
- Program won't parse / raises SyntaxError → exemplars are inconsistent or the
stoptoken fires too late; tighten the format and trim the prompt. - Runs but wrong answer → the reasoning is wrong, not the math; improve the comment-level logic in exemplars, or add sampling + majority vote.
- Infinite loop or timeout → cap execution time; treat a timeout as a failed sample.
- Right code, wrong extraction → your harness and exemplars disagree on the answer variable; unify the convention.
- Good on easy, fails on hard numbers → that's the case PAL is for; verify you're actually executing the code and not reading a number the model wrote.
Testing and how to prove it
Run PAL head-to-head against CoT on a held-out set; the right metric is exact-match answer accuracy. Track three failure buckets separately: generation (no valid program), execution (runtime error/timeout), and reasoning (valid run, wrong answer). The first two are infra problems; the third is a prompt problem.
def evaluate(dataset, run_fn):
correct, gen_err, exec_err = 0, 0, 0
for ex in dataset:
try:
pred = run_fn(ex["question"])
correct += int(str(pred).strip() == str(ex["answer"]).strip())
except SyntaxError:
gen_err += 1
except Exception:
exec_err += 1
n = len(dataset)
return {"accuracy": correct / n, "gen_err": gen_err / n, "exec_err": exec_err / n}
Expect the biggest deltas on large-number and many-step problems — that's where the GSM-Hard 23.1% to 61.2% gap comes from.
Limitations
- Only helps when the answer is computable. For open-ended reasoning or judgment, the program has nothing to execute and PAL reduces to CoT with extra steps.
- Execution is an attack surface. Running model-generated code demands real sandboxing; this is the technique's main operational cost.
- Garbage logic, exact garbage. Deterministic execution guarantees the arithmetic, not the reasoning. A wrong plan produces a precisely wrong number.
- Needs a code-fluent model. Weak or non-code models fail at generation. The interpreter can't fix a program that never parses.
- Domain transfer. Exemplars are domain-shaped; reusing math exemplars for date logic underperforms purpose-built ones.
Advanced techniques
- Self-consistency over programs. Sample N programs, execute all, and majority-vote the answers. This is what takes GSM8K from 72.0% to 80.4%.
- Hybrid PAL + CoT. Let the model reason in prose, then emit code only for the computational sub-steps — useful when part of the task isn't computable.
- Program of Thoughts (PoT). A sibling technique (Chen et al. 2022) that interleaves natural-language reasoning with code; choose it when you want commentary alongside the program.
- Verifier loops. Feed runtime errors back to the model and ask it to repair the program, like a tiny self-debugging agent.
Determinism is not correctness. PAL removes calculation errors, so a confident, exact answer can still be built on flawed reasoning. Don't let the precision of 282 lull you into skipping logic review — validate the plan, not just the number.
Ecosystem and integration
PAL is the canonical "let the model write code, let a tool run it" pattern, and it predates most function-calling APIs. Today you'd implement it with a hosted code-execution tool or a sandboxed subprocess. It composes naturally with:
- Self-consistency — sampling + majority vote over executed programs.
- RAG — retrieve the data, then PAL computes over it.
- Agents — PAL is the "use the Python tool" action inside a larger ReAct-style loop.
Transitioning from CoT to PAL is mostly a prompt swap: replace prose exemplars with code-solution exemplars and add an execution step. Transitioning out of PAL means moving to full tool-use/function-calling when you need external APIs, not just local computation.
Real-world payoff. On GSM-Hard, where word problems carry deliberately large numbers, chain-of-thought with Codex scores 23.1% — it reasons fine but can't do the arithmetic. PAL, running the very same logic as code, scores 61.2%. That near-3x jump is the whole thesis: stop asking the model to be a calculator.
Future directions
- Native code execution baked into model APIs is making the sandbox boundary cleaner and safer.
- Beyond Python — the same split (model plans, runtime computes) extends to SQL, spreadsheets, and domain-specific languages.
- Tighter verify-and-repair loops turn PAL into a self-correcting solver that fixes its own runtime errors.
Summary
- PAL has the model write a program for the reasoning steps and offloads the actual computation to a Python interpreter.
- It removes arithmetic errors, lifting GSM8K from 65.6% (CoT) to 72.0% and beating PaLM-540B by an absolute 15 points (Gao et al., 2022).
- The clearest win is robustness: on GSM-Hard, CoT scores 23.1% and PAL scores 61.2%.
- Sampling N programs and majority-voting pushes GSM8K to 80.4%, edging Minerva-540B's 78.5%.
- Use it when the answer is computable and you can sandbox code; skip it for open-ended generation or when execution isn't safe.
- Determinism guarantees the math, not the logic — review the plan, and never
execuntrusted code unsandboxed.
Read Next
Start reading to get personalized recommendations
Explore Unread
Great job! You've read all available articles