Combining Code Execution with Structured Outputs
The Tension Between Two Features Unit 6 covered structured outputs: constraining a model's response to conform exactly to a JSON schema you define, so your application can parse the result reliably in
The Tension Between Two Features
Unit 6 covered structured outputs: constraining a model's response to conform exactly to a JSON schema you define, so your application can parse the result reliably instead of scraping numbers and labels out of free-form prose. Code interpreter, by contrast, produces exactly the kind of output structured outputs is meant to replace — a code_interpreter_call item full of executed Python, plus a natural-language message summarizing what happened. These two features solve different problems and do not simply combine into "structured code interpreter output" by adding both to one call. Understanding why, and what pattern actually works, is the point of this lesson.
The core issue is that a JSON schema for structured outputs describes the shape of the model's final text message. It has no way to constrain what a tool call does internally — the code the model writes for code_interpreter is not JSON and is not subject to your schema at all. What a schema can do is constrain the final summary the model writes after it has already seen the tool's result. This distinction shapes every pattern in this lesson.
Pattern 1: Structured Final Answer After Tool Use
The most direct approach lets the model use code interpreter freely to do the actual computation, and then constrains only its concluding message to a defined schema:
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class RevenueSummary(BaseModel):
total_revenue: float
top_region: str
top_region_revenue: float
month_over_month_growth_pct: float
response = client.responses.parse(
model="gpt-5.6-terra",
tools=[{
"type": "code_interpreter",
"container": {"type": "auto", "file_ids": [uploaded.id]},
}],
input=(
"Analyze monthly_revenue.csv. Compute total revenue, the region "
"with the highest revenue and its revenue figure, and the average "
"month-over-month growth rate as a percentage."
),
text_format=RevenueSummary,
)
summary = response.output_parsed
print(summary.total_revenue, summary.top_region, summary.month_over_month_growth_pct)
This works because responses.parse (the structured-outputs entry point introduced in Unit 6) still allows tool use during the turn — the model can call code_interpreter as many times as it needs to actually perform the computation, and only its final answer is required to conform to the RevenueSummary schema. The schema does not see or constrain the Python code the model wrote; it only shapes the concluding message, which the model composes after reading the tool's real output.
response.output_parsed gives you a validated RevenueSummary instance — the same guarantee structured outputs provides everywhere else in this course, now sitting on top of a value that was actually computed by executed code rather than estimated by the model. This combination is genuinely powerful: you get both numerical accuracy (from code execution) and a type-safe, predictable shape your application code can consume directly (from structured outputs), without gluing them together yourself.
Note: Support for combining
tools(includingcode_interpreter) withtext_format/responses.parsein a single call, and any constraints on schema complexity when tool use is involved, are the kind of capability that can change between API versions. Confirm current support and any limitations against the official documentation before depending on this pattern in production.
Pattern 2: Two Explicit Steps
For workflows where you want more control — for example, logging the raw analysis separately from the structured extraction, or when the schema needs information that spans a longer exploratory conversation — splitting the work into two calls is more transparent and easier to debug:
analysis = client.responses.create(
model="gpt-5.6-terra",
tools=[{
"type": "code_interpreter",
"container": {"type": "auto", "file_ids": [uploaded.id]},
}],
input=(
"Analyze monthly_revenue.csv thoroughly. Compute total revenue, "
"revenue by region, and month-over-month growth. Explain your "
"findings in plain language."
),
)
extraction = client.responses.parse(
model="gpt-5.6-terra",
input=(
"Extract the following fields from this analysis, using only the "
f"values explicitly stated in it:\n\n{analysis.output_text}"
),
text_format=RevenueSummary,
)
summary = extraction.output_parsed
The first call does the real work with code interpreter enabled and no schema constraint, producing a free-form but computationally grounded explanation. The second call has no tools at all — it is a pure text-to-structured-data extraction over the already computed analysis text, which is a task structured outputs handles very reliably since it is simply reformatting information that is already present in the input rather than performing new reasoning or computation.
This two-step pattern costs an extra API call, but it buys you two things Pattern 1 does not give you as cleanly: you can log and inspect the full, un-truncated analysis independently of the extraction, and you can reuse the same extraction schema across analyses produced by very different prompts, since the extraction step no longer needs to know anything about code interpreter or datasets at all.
Choosing Between the Two Patterns
| Consideration | Pattern 1 (single call) | Pattern 2 (two calls) |
|---|---|---|
| Number of API calls | 1 | 2 |
| Cost and latency | Lower | Higher |
| Visibility into full analysis | Only via output_text/output on the same response | Explicit, separately stored |
| Schema reuse across different analysis prompts | Tied to the same call | Fully decoupled |
| Debugging a wrong extracted value | Requires re-reading one combined response | Can isolate whether the analysis or the extraction was wrong |
Pattern 1 is the right default for most application code — it is simpler and cheaper. Reach for Pattern 2 specifically when you need an audit trail of the full analysis independent of the structured summary, or when the same extraction schema needs to sit on top of analyses generated through several different upstream prompts or even different tools.
A Validation Layer on Top of Either Pattern
Because a schema only guarantees shape, not correctness — a RevenueSummary with a total_revenue field of the wrong value is still perfectly valid JSON — it is worth adding a lightweight sanity check on the structured result before trusting it downstream. This is a preview of the deeper validation techniques covered in Lesson 9:
def sanity_check_summary(summary: RevenueSummary) -> list[str]:
problems = []
if summary.total_revenue <= 0:
problems.append("total_revenue is not positive")
if summary.top_region_revenue > summary.total_revenue:
problems.append("top_region_revenue exceeds total_revenue, which is impossible")
if not (-100 <= summary.month_over_month_growth_pct <= 1000):
problems.append("month_over_month_growth_pct is outside a plausible range")
return problems
def test_sanity_check_summary_flags_impossible_values():
bad_summary = RevenueSummary(
total_revenue=1000.0,
top_region="West",
top_region_revenue=1500.0,
month_over_month_growth_pct=5.0,
)
problems = sanity_check_summary(bad_summary)
assert "top_region_revenue exceeds total_revenue, which is impossible" in problems
print("PASS: sanity_check_summary flags an impossible top_region_revenue value")
def test_sanity_check_summary_accepts_valid_values():
good_summary = RevenueSummary(
total_revenue=1000.0,
top_region="West",
top_region_revenue=400.0,
month_over_month_growth_pct=3.2,
)
assert sanity_check_summary(good_summary) == []
print("PASS: sanity_check_summary returns no problems for consistent values")
test_sanity_check_summary_flags_impossible_values()
test_sanity_check_summary_accepts_valid_values()
sanity_check_summary encodes domain knowledge (a region's revenue cannot exceed the total) that the schema itself has no way to express — Pydantic and JSON Schema can validate types and ranges on individual fields, but not relationships between fields that depend on the meaning of the data. Writing these checks explicitly, and testing them with deliberately constructed valid and invalid examples as shown, is cheap and catches a category of error that structured outputs alone cannot.
Common Mistakes
Believing a valid schema means a correct answer. Structured outputs guarantee the response parses into your defined shape; they say nothing about whether the values inside that shape are numerically correct. Pair structured outputs with the kind of validation shown above and expanded in Lesson 9.
Trying to put the tool's raw code output directly into a structured schema field. A schema field like computation_code: str technically works, but defeats the purpose of structured outputs, which is to give you clean, typed data — not another blob of text to parse further. Extract the result of the computation into typed fields, not the code that produced it.
Using Pattern 2 by default even for simple analyses, paying for two API calls when one would have worked. Start with Pattern 1 and move to Pattern 2 only when you have a concrete reason (auditability, schema reuse) that justifies the extra cost.
Best Practices
Default to a single responses.parse call with both tools and text_format for straightforward analysis-to-structured-result workflows, reserving the two-step pattern for cases with a specific need for separation.
Add cross-field sanity checks on structured results derived from computation, since schema validation alone cannot catch logically impossible combinations of otherwise well-typed values.
Keep structured schemas focused on the final answer, not the process. Fields should represent conclusions ("total_revenue", "top_region") rather than intermediate artifacts of how code interpreter arrived at them, keeping the schema stable even if the underlying analysis approach changes.