When your LLM returns a tool invocation, parsing function call arguments python correctly is the difference between a silent pipeline failure and a system that degrades gracefully. The arguments arrive as a JSON-encoded string inside a tool call object, and that string is generated by a stochastic model—not a compiler.
Step 1: Capture the raw function call payload
An OpenAI-compatible endpoint returns tool_calls in the message object. Whether you call the vendor directly or route through a gateway such as n4n.ai, the schema is identical: a list of objects with function.name and function.arguments.
from openai import OpenAI
client = OpenAI() # or point base_url at your gateway
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Book a flight to SF tomorrow"}],
tools=[{
"type": "function",
"function": {
"name": "book_flight",
"parameters": {
"type": "object",
"properties": {
"destination": {"type": "string"},
"date": {"type": "string"}
},
"required": ["destination", "date"]
}
}
}]
)
msg = resp.choices[0].message
if not msg.tool_calls:
raise RuntimeError("Model did not call a function")
Extract the first tool call. In production, loop and dispatch multiple calls; for this walkthrough we handle one.
Step 2: Extract the arguments string
The arguments field is a string, not a dict. This surprises engineers who expect parsed JSON. Pull it out and log the raw value before parsing.
import json
tool_call = msg.tool_calls[0]
name = tool_call.function.name
raw_args = tool_call.function.arguments
print(f"Invoked {name} with raw: {raw_args!r}")
If raw_args is an empty string, the model emitted a call with no parameters. Decide explicitly whether that is valid for your function.
Step 3: Parse with strict JSON and contain the damage
Never pass the string to eval or ast.literal_eval as a shortcut. Use json.loads inside a try/except. Catch json.JSONDecodeError and return a structured error that the model can consume on the next turn.
def parse_args(raw: str) -> dict:
try:
return json.loads(raw)
except json.JSONDecodeError as e:
# Log with context, do not crash the request thread
raise ValueError(f"Malformed JSON in function call: {e}") from e
try:
args = parse_args(raw_args)
except ValueError as e:
# Feed this back to the model as a tool error message
error_payload = {"error": str(e)}
Why strictness matters
Models sometimes emit trailing commas or single quotes. If you control the prompt, forbid that. If you do not, write a minimal normalizer only after strict parse fails—but keep it narrow and audited.
import re
def lenient_parse(raw: str) -> dict:
try:
return json.loads(raw)
except json.JSONDecodeError:
# Strip trailing commas before closing braces
cleaned = re.sub(r",\s*([}\]])", r"\1", raw)
return json.loads(cleaned)
Use lenient_parse only when strict parse fails and you have tests proving it handles your observed model errors.
Step 4: Validate against an explicit schema
Parsing gives you a dict with unknown types. Enforce structure with Pydantic. Define a model that mirrors the tool’s declared parameters.
from pydantic import BaseModel, ValidationError
class BookFlightArgs(BaseModel):
destination: str
date: str
try:
validated = BookFlightArgs(**args)
except ValidationError as e:
raise ValueError(f"Schema violation: {e}") from e
Coercion vs rejection
Pydantic coerces types where safe (e.g., int from string) but rejects nonsense. For date fields, use datetime.date with a validator to catch "tomorrow" which the model may emit despite your schema.
from datetime import date, datetime, timedelta
from pydantic import field_validator
class BookFlightArgs(BaseModel):
destination: str
date: date
@field_validator("date", mode="before")
def parse_flexible_date(cls, v):
if isinstance(v, str):
if v.lower() == "tomorrow":
return date.today() + timedelta(days=1)
return datetime.fromisoformat(v).date()
return v
Step 5: Bind to your local function
Maintain a registry mapping function names to callables. Do not use globals() lookup in production.
def book_flight(destination: str, date: date) -> dict:
# Actually call your booking API
return {"status": "booked", "destination": destination, "date": str(date)}
REGISTRY = {
"book_flight": book_flight,
}
def dispatch(name: str, args: dict):
if name not in REGISTRY:
raise ValueError(f"Unknown function: {name}")
return REGISTRY[name](**args)
result = dispatch(name, validated.model_dump())
This separation lets you unit-test parsing and dispatch without network calls.
Step 6: Handle missing and extra fields
Models ignore your required array or add fields they invented. Configure Pydantic to forbid extras or ignore them based on your tolerance.
class BookFlightArgs(BaseModel):
model_config = {"extra": "ignore"}
destination: str
date: date
If a required field is missing, ValidationError fires. Catch it and return the error text to the model so it can retry with corrected arguments. That retry loop is where parsing function call arguments python intersects with orchestration logic.
Step 7: Return errors as tool messages
The model cannot fix what it cannot see. When parsing or validation fails, emit a tool message with the error and continue the conversation.
error_msg = {
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps({"error": "date must be ISO format or 'tomorrow'"}),
}
# Send error_msg back in the next completion call
This pattern turns a hard failure into a self-correcting loop. Gate the number of retries to avoid infinite spins.
Step 8: Verify success with tests
Write tests that exercise each step with real captured payloads. Use pytest.
def test_parse_valid():
raw = '{"destination": "SF", "date": "2025-01-02"}'
args = BookFlightArgs(**parse_args(raw))
assert args.destination == "SF"
def test_parse_invalid_date():
raw = '{"destination": "SF", "date": "next tuesday"}'
try:
BookFlightArgs(**parse_args(raw))
assert False, "should have raised"
except ValueError:
pass
Run pytest -q and confirm green. Additionally, log a histogram of JSONDecodeError reasons in production; if a new model version changes its quoting style, you will see it before users do.
Practical notes on parsing function call arguments python at scale
Batching many tool calls in one response multiplies the parsing surface. Parallelize validation with a worker pool only if profiling shows JSON parsing as a bottleneck—it rarely is. The bigger risk is schema drift: when you update the Python function signature, update the Pydantic model and the tool definition in the same commit. Mismatch there is the most common source of silent extra field drops.
If you use an inference gateway that honors client routing directives, you can pin a model version to keep argument style stable. n4n.ai forwards provider cache-control hints and meters per-token usage, which makes it cheap to replay failed conversations during debugging. That replay is how you collect the malformed JSON samples that justify a lenient_parse branch.
Finally, never log full arguments without scrubbing PII. The parsing layer is the right place to redact before the payload hits your observability stack.
Stick to strict JSON, validate with types, and feed errors back. That discipline keeps your function-calling layer boring—which is exactly what you want.