TAI BUI
Back to curriculum
CapstonePython

Code Exec Metric

Generated code is right when it passes the tests. The eval harness has to extract the code, run it without crashing the host, and tally pass rates honestly. Build that surface.

TIME: ~90 minPREREQUISITES: Phase 19 Track B foundations, lessons 70 and 71

Learning Objectives

  • Extract a code block from a free-form generation in a way that matches the post-process rule from lesson 70.
  • Execute candidate code in an isolated subprocess with a wall-clock timeout, output cap, and an import denylist.
  • Score a task as the fraction of supplied assertion strings that pass against the candidate.
  • Compute pass-at-k for tasks that sample multiple generations from one model.
  • Treat sandbox crashes, syntax errors, and timeouts as first-class fail modes with distinct exit codes the runner can log.

Why an isolated subprocess

Isolating a candidate means running it in a fresh Python interpreter, one per candidate, so whatever it does happens in a process the host can kill. Inline exec has no such boundary. A generated while True: pass blocks the eval forever, and a generated import shutil; shutil.rmtree('/') does exactly what it says. The runner embeds the candidate into a script with repr(), runs that script in a fresh python -I -c interpreter, reads the assertion results back from stdout, and kills the process if it overruns. The host eval process keeps running.

HumanEval, MBPP, BigCodeBench, and LiveCodeBench all use a subprocess sandbox, and some layer Docker on top. I stop at the subprocess because it is portable, it is stdlib, and it catches the failure modes that matter for an educational eval. Production deployments add seccomp, network isolation, and a read-only filesystem. That hardening work lives outside this track.

Check: Why an isolated subprocess0/2
1. In this lesson, what does isolating a candidate mean?Define
2. Why does the runner spawn a fresh Python subprocess per candidate rather than calling exec inline?Explain

The shape of a code-exec task

A code_exec task is one whose targets are assertion strings instead of reference text. The runner extracts a fenced code block from the generation, builds a test harness around it, and runs the result. extract_code returns the contents of the first fenced block. If the text has no fences at all it returns the whole text, and if a fence is opened but never closed it returns None, which scores 0.0 without running anything.

Mermaid diagram

The score is a fraction in [0, 1]: assertions passed over assertions supplied. A task with three assertions where two pass scores 0.667. The runner returns the same shape whatever fails. Subprocess crashes map to a normalised error code, so a Python traceback does not bubble up to the harness.

Check: The shape of a code-exec task0/4
1. What does extract_code return for a generation that contains a closed fenced block?Define
2. A task has three assertions. The candidate passes two and fails one. What score does the task get?Apply
3. What does score_code_exec return for the prediction "```python\nbroken" with the assertion list ["True"]?Apply
4. A reply holds a closed block with solution A, then some prose, then a second closed block with solution B. Which solution does the runner execute?Apply

The denylist

The denylist is a set of modules candidate code is not allowed to import. It is import-based: before running the candidate, the runner script replaces builtins.__import__ with a guard that raises ImportError("denied module: <name>") for any denied module or submodule of one. The list is deliberately conservative: subprocess, socket, requests, urllib (with its request, error, and parse submodules), ctypes, shutil, http.client, asyncio.subprocess. os.system is not an import, so it is patched separately to raise OSError.

I don't pretend this is bulletproof. Determined adversarial code can escape any in-process sandbox in Python, so the denylist is a backstop. The wall-clock timeout and the output cap carry the load.

python
DENIED = {
    "os.system": True,
    "subprocess": True,
    "socket": True,
    "shutil": True,
    "requests": True,
    "urllib": True,
    "ctypes": True,
}

This dict is illustrative. The real set lives in the runner template in main.py. The guard and the os.system patch run before the candidate is compiled, so a denied import fails when the candidate executes and surfaces as an error exit code, not syntax_error.

Check: The denylist0/5
1. What is the denylist in the code-exec runner?Define
2. Why does the lesson call the denylist a backstop rather than the main protection?Explain
3. Why does a candidate that imports a denied module come back as error and not syntax_error?Explain
4. Why does the import guard not stop a candidate from calling os.system?Explain
5. Every candidate for one task scores 0.0 with exit code error and a detail containing "denied module: shutil". What is the cause?Analyze

Wall-clock timeout

The wall-clock timeout is the budget of real elapsed seconds a candidate gets before the runner kills it. The default is three seconds, enforced by a subprocess.Popen plus a select loop that reads stdout against a monotonic deadline. When the deadline passes, the runner kills the process and records a timeout exit reason for the task. The task scores zero and the runner moves on.

The budget is the timeout_s argument to run_candidate and score_code_exec. Long-running unit tests can ask for more, and main.py clamps the value to the range 0.1 to 30 seconds to keep the suite bounded.

Check: Wall-clock timeout0/4
1. What does the wall-clock timeout measure?Define
2. How does the runner enforce the wall-clock timeout on a candidate that loops forever?Explain
3. A caller passes timeout_s=120 to run_candidate. What does main.py do with it?Apply
4. A reviewer says the timeout is redundant because the denylist already blocks subprocess and socket. Which candidate exposes the flaw?Analyze

Output cap

The output cap is a hard limit on how much stdout a candidate can produce, set at 256 KB. Without one, a subprocess can flood stdout and exhaust host memory. The runner streams stdout into a buffer and kills the child as soon as the running total crosses the cap. The result is recorded as exit_code = error with the detail string "output overflow". In practice this shows up when a generation accidentally writes an infinite loop that prints.

Check: Output cap0/3
1. What does the output cap limit?Define
2. How does the runner report a candidate whose stdout passes the output cap?Explain
3. A run is logged as exit code error with detail "output overflow", and the candidate's logic looked fine. What most likely happened?Analyze

Pass-at-k

Pass-at-k is the probability that at least one of k sampled solutions passes. The unbiased estimator used by HumanEval and friends computes it from n independent samples per task, c of which pass. The probability that a sample of size k drawn from the n contains at least one passing solution is:

pass_at_k(n, c, k) = 1 - C(n - c, k) / C(n, k)

When n - c < k the binomial in the numerator is zero, so the value is 1. The implementation handles that edge case directly. If k > n it returns float(c > 0), and invalid arguments (negative counts, k <= 0, or c > n) raise ValueError. pass_at_k(n, c, k) is exposed for the leaderboard layer in lesson 74.

Mermaid diagram

Check: Pass-at-k0/5
1. What does pass-at-k measure for a single task?Define
2. Why does pass_at_k return exactly 1 when n - c < k?Explain
3. A task has n=10 samples and c=3 pass. What is pass_at_k(10, 3, 8)?Apply
4. Two tasks each have three samples: [True, False, False] and [True, True, False]. What does pass_at_k_estimator report for k=1?Apply
5. The demo scores three tasks with ten samples each, where 2, 1 and 0 samples pass. What is the mean pass@10?Apply

Exit codes

An exit code is a label for how a run ended, recorded as metadata beside the score. The runner returns one of five outcomes per task:

  • pass when all assertions passed.
  • assertion_fail when the code ran but at least one assertion failed.
  • syntax_error when compile() raises a SyntaxError on the candidate.
  • timeout when the wall clock expired.
  • error for any other crash, including denylist hits (an ImportError raised when the candidate runs), exceptions at exec time, a non-zero subprocess exit, and output overflow (overflow surfaces with detail "output overflow").

The score is still a fraction. Downstream lessons can decide whether a timeout counts as zero or as missing data.

Check: Exit codes0/8
1. Which exit code does the runner return when the candidate code does not parse?Define
2. In this runner, what is an exit code?Define
3. Why does the author log the exit code and detail next to the score instead of keeping just the fraction?Explain
4. A candidate's first two lines are `import sys` and `sys.exit(3)`, followed by a correct add(). The assertion is `add(1,2) == 3`. What does the runner report?Apply
5. The candidate is `def add(a,b): raise RuntimeError('boom')` and the assertion is `add(1,2) == 3`. What does the runner report?Apply
6. run_candidate is called with correct code and an empty assertion list. What does it return?Apply
7. Every assertion calls add(...), but a correct candidate defines the function as add_numbers. What does the runner report?Analyze
8. A leaderboard must not punish a model for timeouts that a slow host may have caused. What should the pipeline do?Design

What the harness does not do

The harness does not give you a real sandbox. It does not run untrusted code from the open web, and it does not handle stateful tasks like file I/O or network calls. Those need a container or a microVM. What you get is the contract: an isolated subprocess, a denylist, a timeout, an output cap, a clean exit-code vocabulary, and pass-at-k math.

Check: What the harness does not do0/1
1. You must score code from the open web that writes files and opens network connections. Which design fits the lesson's limits?Design
Author's Note

My bet is that the timeout and the output cap deserve your review time before the denylist does. The denylist blocks direct imports of the modules on its list, but the deadline and the 256 KB limit are what stop a misbehaving candidate from hanging or flooding the host. In main.py I'd read the streaming loop in run_candidate twice.

I'd also log the exit code and detail next to the score instead of keeping just the fraction. ExecResult carries both, and a timeout, a syntax error, and a failed assertion can all come back with a low score for different reasons. Whether a timeout counts as zero or as missing data is a decision I'd make on purpose downstream.

How to read the code

main.py defines extract_code, run_candidate, score_code_exec, and pass_at_k. The subprocess runner script is built as a string and passed as -c to a fresh Python interpreter. The tests in code/tests/test_exec.py exercise the exit codes plus pass-at-k against worked examples drawn from the HumanEval style.

Read main.py top to bottom. The runner template is the load-bearing piece. Keep reading the assertion loop until you can predict the JSON envelope it writes back to the parent process.

Going further

Once the subprocess shape works, portability is the next problem. The runner reads the child's stdout with os.set_blocking and select, which is POSIX-oriented, so it is not portable as written. The cleanest fix is to put the runner in a Docker image. After that, replace assertion strings with real unit test files so the eval matches what production CI does. At that point, stop calling assertion strings tests. They are toy tests with toy failure modes.

Warm-up0/3
1. A model replies with a short explanation followed by a fenced code block. What must a harness do before running anything?Warm-up
2. A fair coin is flipped twice. What is the probability that at least one flip lands heads?Warm-up
3. A Python file contains the line `def add(a, b)` with the colon missing. When does Python report the error?Warm-up

Code