How to Handle Gemini 2.5 Pro Code Execution in a Type-Safe Way on the Backend
Honestly, I was confused when I first received a Code Execution response. I tried pulling it out with response.text, got None, and spent a long time puzzling over it. It turns out that code execution responses come back split into multiple Part objects of different types inside candidates[0].content.parts. If you don't know this and treat it like a regular text response, your entire pipeline breaks silently.
Gemini Code Execution is a tool where the model runs Python code in Google's isolated sandbox and returns the results as structured Part objects. Unlike regular Function Calling where the developer executes a function externally, the API backend handles code execution entirely. The key point from a server perspective is that you receive a "response with guaranteed code execution results."
This post covers how to handle response parts by type, how to handle outcome branching, and what to watch out for in production when integrating Code Execution into a backend service. (Code examples in this post assume the latest version of the google-genai SDK as of September 2026; examples using match syntax require Python 3.10 or higher.)
Why Code Execution Is Different — Start by Understanding the Response Structure
A regular generate_content call ends cleanly with response.text, but when Code Execution is enabled, the response is split into multiple Part objects of different types.
| Part Type | Field | Content |
|---|---|---|
Part.text |
text (str) |
Model's explanatory/interpretive text |
Part.executable_code |
language, code |
Python code generated and executed by the model |
Part.code_execution_result |
outcome, output |
Execution result status code and standard output |
Here, outcome is not a string but an enum. It has type google.genai.types.Outcome with values OUTCOME_UNSPECIFIED, OUTCOME_OK, OUTCOME_FAILED, and OUTCOME_DEADLINE_EXCEEDED. Among these, OUTCOME_UNSPECIFIED is a defensive default value observed in cases of SDK/server protocol mismatches or incomplete responses, so it's safer to explicitly handle it as an unknown state in your branching logic.
Within a single generate_content request, the model can repeat the cycle of code generation → execution → result check → revision multiple times (the official documentation does not specify an exact upper limit on the number of iterations, and time limits per execution are as noted in the Gemini API documentation). This means the response parts may not be just one, but a sequence like [text, executable_code, code_execution_result, text, executable_code, code_execution_result, ...]. If you don't account for this when writing your initial parsing code, you'll only capture the first result and stop there.
Whether the model changes its approach and retries after a timeout is not clearly defined behavior in the official documentation and relies on observation. In server code, it's safest to leave open the possibility that "parts may continue after a timeout" and treat the last code_execution_result's outcome as the final state.
Basic Call — Start by Putting ToolCodeExecution in config
from google import genai
from google.genai.types import Tool, ToolCodeExecution, GenerateContentConfig
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents="Run code using pandas to calculate the mean and standard deviation of a list of numbers.",
config=GenerateContentConfig(
tools=[Tool(code_execution=ToolCodeExecution())]
),
)This uses the official types from the google-genai package (pip install google-genai). ToolCodeExecution() takes an empty constructor and simply activates the tool.
Iterating Over Response Parts — Branching on outcome
The basic pattern after receiving a response is to iterate through parts and process each type accordingly. While multiple parsing styles are possible, this post sticks to one pattern: collect all parts in order and treat the last code_execution_result's status as the final result. This is because intermediate information (retry flow, failure logs) is also preserved, which is good for observability.
from google.genai.types import Outcome
def parse_code_execution_response(response):
result = {
"texts": [],
"codes": [],
"outputs": [],
"final_outcome": None,
}
for part in response.candidates[0].content.parts:
if part.text is not None:
result["texts"].append(part.text)
elif part.executable_code is not None:
result["codes"].append({
"language": part.executable_code.language,
"code": part.executable_code.code,
})
elif part.code_execution_result is not None:
outcome = part.code_execution_result.outcome
output = part.code_execution_result.output
result["outputs"].append({
"outcome": outcome,
"output": output,
})
result["final_outcome"] = outcome
return result
parsed = parse_code_execution_response(response)
# match syntax requires Python 3.10 or higher.
match parsed["final_outcome"]:
case Outcome.OUTCOME_OK:
print("Success:", parsed["outputs"][-1]["output"])
case Outcome.OUTCOME_FAILED:
print("Execution failed. Last output:", parsed["outputs"][-1]["output"])
case Outcome.OUTCOME_DEADLINE_EXCEEDED:
print("Execution timed out. Consider reducing computation or splitting input data.")
case Outcome.OUTCOME_UNSPECIFIED | None:
print("Response is incomplete or in an unknown state.")
case _:
print("Unknown status:", parsed["final_outcome"])The reason for using is not None instead of a truthiness check like if part.text: is to avoid missing empty string or empty code parts from a pipeline observability standpoint. Relying on truthiness in type-safety-oriented code leads to subtle bugs later.
Example: Attaching a Data Analysis API
A genuinely useful case is "an API that automatically aggregates data when a user pastes in CSV content." Code Execution is well-suited for this because instead of trusting text generated by the model, you return only the actually executed results to the client.
This follows the same parsing pattern as the previous section; analyze_csv_data is just a thin wrapper on top of it.
import json
from google import genai
from google.genai.types import (
Tool, ToolCodeExecution, GenerateContentConfig, Outcome,
)
client = genai.Client()
def _strip_markdown_fence(text: str) -> str:
# Models often wrap JSON with ```json ... ```.
# This logic safely handles only a single code fence.
# It may behave incorrectly with nested fences or triple backticks within the body.
stripped = text.strip()
if stripped.startswith("```"):
stripped = stripped.split("\n", 1)[1] if "\n" in stripped else stripped
stripped = stripped.rsplit("```", 1)[0]
return stripped.strip()
def analyze_csv_data(csv_content: str, user_question: str) -> dict:
prompt = f"""Analyze the following CSV data and answer the question.
Output the result strictly in JSON format.
CSV Data:
{csv_content}
Question: {user_question}"""
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=prompt,
config=GenerateContentConfig(
tools=[Tool(code_execution=ToolCodeExecution())]
),
)
parsed = parse_code_execution_response(response)
if not parsed["outputs"]:
return {"status": "error", "outcome": "no_result", "output": ""}
last = parsed["outputs"][-1]
if last["outcome"] != Outcome.OUTCOME_OK:
return {
"status": "error",
"outcome": str(last["outcome"]),
"output": last["output"],
}
raw_output = _strip_markdown_fence(last["output"])
try:
data = json.loads(raw_output)
except json.JSONDecodeError as e:
# Model did not produce valid JSON. The caller decides whether to retry or fall back.
return {
"status": "error",
"outcome": "invalid_json",
"output": raw_output,
"error": str(e),
}
return {"status": "ok", "data": data}Code fence stripping is fundamentally fragile. If the model output contains triple backticks within the body or uses nested fences, the logic above will cut incorrectly. A more robust alternative is to strongly instruct the prompt to "output pure JSON without any fences," or to use a response schema (Structured Output) alongside it. Compatibility between Code Execution and response schemas varies by SDK version, so validation in a real environment is necessary.
Production Pipeline — Handling HTTP 429 and Retries
Traffic spikes can trigger 429 errors, so you'll need exponential backoff retry logic. Check the Gemini API official rate limits page first to confirm the exact limits applied to your account and model.
Example for a synchronous backend:
import time
import google.api_core.exceptions
def call_with_backoff(client, model, contents, config, max_retries=4):
delay = 1.0
for attempt in range(max_retries):
try:
return client.models.generate_content(
model=model,
contents=contents,
config=config,
)
except google.api_core.exceptions.ResourceExhausted:
if attempt == max_retries - 1:
raise
time.sleep(delay)
delay *= 2google.api_core.exceptions.ResourceExhausted is the exception class that maps to HTTP 429. If you use this function as-is in an async backend based on FastAPI, aiohttp, or asyncio, time.sleep will block the event loop. In async environments, replace it with asyncio.sleep and use the SDK's async client.
import asyncio
async def call_with_backoff_async(client, model, contents, config, max_retries=4):
delay = 1.0
for attempt in range(max_retries):
try:
return await client.aio.models.generate_content(
model=model,
contents=contents,
config=config,
)
except google.api_core.exceptions.ResourceExhausted:
if attempt == max_retries - 1:
raise
await asyncio.sleep(delay)
delay *= 2Tradeoffs — Things to Confirm with Your Team Before Adopting
| Item | Pros | Cons / Caveats |
|---|---|---|
| Execution environment | Google-managed sandbox, no infrastructure impact | Stateless — variables and files do not persist between calls |
| Language support | numpy, pandas, matplotlib, etc. pre-installed | Python only |
| Auto-correction | Model revises and re-executes code on failure | With repeated executions, previous code and output accumulate in context, increasing input tokens. More retries mean higher billed tokens per request |
| Network | Secure due to isolated environment | External API calls and pip install blocked by default inside the sandbox |
| Execution time | Per-execution time limit exists | OUTCOME_DEADLINE_EXCEEDED can occur with large data or complex computations |
| Response type | Clear branching via outcome enum |
Fallback handling and timeout handling are the developer's responsibility |
There are two most common mistakes.
First, trying to retrieve the response with response.text and getting None. With a response where Code Execution is enabled, you must iterate through parts directly.
Second, ignoring cases where outcome is not OUTCOME_OK. In particular, execution timeouts (OUTCOME_DEADLINE_EXCEEDED) can appear suddenly as data size grows. Returning output as-is without any handling in this case delivers empty results or confusing messages to the client.
Handling Image Parts — inline_data Is Already bytes
When the model draws a chart with Matplotlib, the PNG binary comes in as Part.inline_data. For a report auto-generation pipeline, you can add this to your iteration logic.
In the google-genai SDK, Part.inline_data.data is already returned as bytes, so you must not perform base64 decoding again. Just write it to a file as-is.
def save_inline_images(response, prefix="chart"):
saved = []
for idx, part in enumerate(response.candidates[0].content.parts):
if part.inline_data is not None and part.inline_data.data:
mime = part.inline_data.mime_type or "application/octet-stream"
ext = "png" if mime.endswith("png") else "bin"
path = f"{prefix}_{idx}.{ext}"
with open(path, "wb") as f:
f.write(part.inline_data.data)
saved.append((path, mime))
return savedOptions to Consider When Scaling
If you're planning to build a multi-step agent beyond a single-call pipeline, it's worth looking at the code execution tool in the Google Agent Development Kit (ADK). Connecting it as a tool in an agent declaration simplifies multi-agent pipeline integration (pip install google-adk).
If you have enterprise requirements such as state persistence, VPC Service Controls integration, or large file handling, you can consider the Vertex AI code execution option. Specific limits (state persistence duration, file size limits, etc.) vary by documentation and your organization's contract terms, so check the latest official documentation for accurate figures before adopting.
When to Use It and When Not to
Code Execution delivers value under the following conditions:
- When result accuracy matters more than the smoothness of text generation (numerical calculations, CSV/JSON aggregation, statistics)
- When you don't want to manage sandbox operations, isolation, and security yourself
- When responses can be handled within the scope of pre-installed libraries (numpy, pandas, matplotlib, etc.)
On the other hand, other approaches are better in these cases:
- When you need to preserve state (variables, files) across calls → self-managed sandbox or Vertex AI option
- When you need to make external API calls or install arbitrary packages → self-managed execution environment or delegating external code via Function Calling
- When you need to execute languages other than Python
- When computations are large enough to exceed the execution time limit → split data or separate into a batch processing path
The server-side integration checklist is short. Iterate through parts to separate the three types, branch on the outcome enum, and treat 429 errors, timeouts, and JSON parse failures as distinct failure modes. Lock down these three things, and the rest can be layered on to fit your application's characteristics.
References
- Code execution | Gemini API | Google AI for Developers
- Execute code with the Gemini API | Vertex AI | Google Cloud
- Gemini API Code Execution tool for ADK — Agent Development Kit
- Rate limits | Gemini API | Google AI for Developers
- googleapis/python-genai — GitHub
- Google Gen AI SDK Official Documentation