# Function calling and structured outputs
The fastest way to turn a chatbot into a system is to hand the model a set of functions it can call and demand its answers come back as strict JSON your code can trust. This lesson goes deep on both mechanisms in the OpenAI APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.Voir la définition complète →: tool calling (the loop where the model asks your code to run something) and Structured Outputs (the guarantee that the model's response matches your schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète →). They solve different problems and they compose beautifully.
Keep these separate in your head:
You often use both: tools to gather facts, Structured Outputs to package the result.
A tool is a JSON description of a function: a name, a description, and a parameter schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète →. The description is not decoration. The model reads it to decide *when* and *how* to call the function, so write it like documentation for a junior developer.
from openai import OpenAI
client = OpenAI()
tools = [{
"type": "function",
"name": "get_weather",
"description": "Get the current temperature for a city in Celsius.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Lisbon'"}
},
"required": ["city"],
"additionalProperties": False
}
}]Two details matter. required lists arguments the model must supply, and additionalProperties: False blocks the model from inventing extra fields. Both push toward predictable calls.
Here is the part people get wrong: the model does not run your function. It returns a *request* to call it. You run the function, feed the result back, and call the model again. That round trip is the loop, and you own it.
The flow:
1. Send the user message plus your tool definitions.
2. The model replies with one or more function_call items (or a normal answer if no tool is needed).
3. Your code executes each call and appends a function_call_output with the result.
4. You call the model again with the updated input. It now writes the final answer.
import json
def get_weather(city):
# Pretend this hits a real API.
return {"city": city, "temp_c": 19}
input_list = [{"role": "user", "content": "What's the weather in Lisbon?"}]
response = client.responses.create(
model="gpt-4.1",
tools=tools,
input=input_list,
)
# Carry the model's output forward as part of the conversation.
input_list += response.output
for item in response.output:
if item.type == "function_call":
args = json.loads(item.arguments)
result = get_weather(**args)
input_list.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result),
})
final = client.responses.create(
model="gpt-4.1",
tools=tools,
input=input_list,
)
print(final.output_text)Note call_id. The model can request several tool calls in one turn (parallel tool calling), and each output must be matched back to its call by id. Append every result before you make the follow-up request.
while loop that continues until no function_call items come back. Cap it (say, 8 iterations) so a confused model cannot spin forever.get_order_status(order_id) beats one mega-function with a mode flag. Narrow tools are easier for the model to choose correctly and easier for you to secure.tool_choice="auto" (default), "required" to force *some* tool, or name a specific tool to force exactly that one. Set parallel_tool_calls=False if your tools must run in sequence.Tool calling handles *actions*. Structured Outputs handles the *shape of the answer*. When you set strict: true and supply a schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète →, the APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.Voir la définition complète → constrains generation so the output provably matches your schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète →. This is stronger than the old "respond in JSON" prompt trick, which produced valid JSON most of the time and broke at 2am.
The cleanest path in Python is to define your schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète → as a Pydantic model and let the SDK parse it for you.
from pydantic import BaseModel
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
response = client.responses.parse(
model="gpt-4.1",
input=[
{"role": "system", "content": "Extract the event details."},
{"role": "user", "content": "Standup with Ana and Rui on Friday."},
],
text_format=CalendarEvent,
)
event = response.output_parsed
print(event.participants) # ['Ana', 'Rui']output_parsed hands you a typed object, not a string you have to json.loads and pray over. If you are not in Python, you pass a raw JSON SchemaSchemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète → in text.format with "type": "json_schema" and "strict": true, and you get back guaranteed-conformant JSON text.
Optional[str] in Pydantic, then check for null in your code.status can only be open, pending, or closed, define it as an enum. The model cannot return in-progress and surprise you downstream.minimum, maximum, and some format constraints may not be enforced. Check the Structured Outputs guide before relying on a keyword.Most production features use the two together. Imagine a support assistant that answers a refund question:
1. The model calls lookup_order(order_id) (tool calling) to fetch real data.
2. You return the order record.
3. The model produces a final answer constrained to a RefundDecision schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète → (Structured Outputs): a boolean eligible, an enum reason, and a customer_message string.
Your application code never parses free text. It reads decision.eligible and branches. That is the whole point: the model handles language and judgment, your code handles control flow, and the boundary between them is a typed contract.
A subtlety worth knowing: tool *parameters* and Structured *Outputs* are separate schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.Voir la définition complète → slots. One shapes the call going in, the other shapes the answer coming out. You can use either alone or both at once in the same request.
Vérification des acquis
1. According to the lesson, what is the primary purpose of Structured Outputs in the OpenAI API?
2. In the lesson, why is a tool's 'description' field described as 'not decoration'?
3. The lesson notes that the course uses the Responses API. What does it say about the older Chat Completions API?
4. Select ALL correct answers about how function (tool) calling works according to the lesson.
Sélectionnez toutes les réponses correctes.
5. Select ALL correct answers about the relationship between tool calling and Structured Outputs as described in the lesson.
Sélectionnez toutes les réponses correctes.
Real systems break in specific ways. Plan for them.
{"error": "order not found"}) rather than throwing. The model can read that and recover, asking the user for a correct order id.output_parsed. Handle it explicitly instead of treating a refusal as malformed data.incomplete and raise your max_output_tokens for large objects.You have now seen the raw machinery. Higher-level OpenAI products are built on exactly these primitives:
while loop by hand. When your tool orchestration gets complex (multiple agents, guardrails, tracing), graduate to it rather than maintaining bespoke loop code. See the Agents SDK docs.Understanding the bare loop first means none of these feel like magic. They are conveniences over the contract you just learned.
call_id, and calls the model again. Loop until no tool calls remain, with a hard iteration cap.output_parsed.tools