What Function Calling Is
The Problem: A Model That Can't Actually Do Anything
Every capability this course has covered so far — text generation, structured outputs, reading images and documents, generating images and speech — has one thing in common: the model reads input and produces output, and nothing in the outside world changes as a result. The model has no way to look up today's exchange rate, check a real inventory database, send an email, or run a calculation against live data. It can only work with what was included in the request and whatever it happens to know from training, which for a fast-moving fact (an account balance, a stock price, whether a specific flight is delayed) is not knowledge the model can have at all, no matter how capable it is.
Function calling (sometimes called "tool calling") is the mechanism that closes this gap. It lets you describe, as part of a request, a set of functions the model is allowed to ask to have called — and the model, when it decides one of those functions would help answer the current request, responds not with a final answer but with a structured request naming which function it wants called and with what arguments. Your own code then actually runs that function, and feeds the result back to the model, which can use it to produce a final answer or ask for another function call.
What Function Calling Is Not
A common misconception worth clearing up immediately: the model itself never executes any code. It cannot reach out to the internet, query a database, or run a Python function on your machine. Function calling only ever produces a structured description of a function call — a name and a set of arguments — as part of the model's response. Your application code is entirely responsible for interpreting that description, deciding whether to actually run anything, executing the real function if it does, and sending the result back. This distinction matters a great deal for security and control, which Lesson 5 covers in depth: because the model can only request a call, never make one directly, your code retains full control over what actually happens, including the ability to refuse a request, sanitize its arguments, or substitute a safer implementation.
A Minimal End-to-End Example
tools = [
{
"type": "function",
"name": "get_current_temperature",
"description": "Get the current temperature for a given city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "The city name, e.g. 'Boston'"},
},
"required": ["city"],
"additionalProperties": False,
},
}
]
response = client.responses.create(
model="gpt-5.6-terra",
input="What's the temperature in Boston right now?",
tools=tools,
)
for item in response.output:
if item.type == "function_call":
print(f"Model wants to call: {item.name}")
print(f"With arguments: {item.arguments}")
Running this produces something like Model wants to call: get_current_temperature and With arguments: {"city": "Boston"} — the model has recognized that answering the question requires information it cannot know on its own, and instead of guessing a plausible-sounding temperature (a real risk without function calling, since a model asked a factual question it can't answer will sometimes produce a confident but fabricated answer), it has requested the specific piece of information it needs, in a precise, machine-readable form.
Notice what has not happened yet: no temperature has actually been looked up, and no final answer has been produced. This first response only contains the model's request to call a function — completing the interaction requires your code to actually run get_current_temperature("Boston") (an operation with a real implementation that reaches an actual weather source, not something the model does), and then send that result back to the model in a follow-up call, which Lesson 3 covers as "the full loop."
The Structure of a Tool Definition
Each entry in the tools list follows a consistent shape worth understanding piece by piece, since Lesson 2 covers writing these schemas in much greater depth:
| Field | Purpose |
|---|---|
type | Identifies this as a "function" tool, distinguishing it from other tool types (such as the built-in tools Unit 9 covers). |
name | The identifier the model will use to refer to this function when it wants to call it — this should match the actual function name in your code, by convention, though technically it just needs to be a consistent string your code recognizes. |
description | A natural-language explanation of what the function does and when it's useful — this is the primary signal the model uses to decide whether to call this function at all, making it one of the most consequential pieces of text in the whole tool definition. |
parameters | A JSON Schema (the same schema format Unit 6 used for structured outputs) describing what arguments the function accepts, their types, which are required, and any constraints on their values. |
The description field deserves particular attention: a vague description ("gets weather info") gives the model little basis for deciding when this function is relevant, while a precise one ("Get the current temperature for a given city, in Fahrenheit, using live weather data") tells the model both what the function does and, implicitly, when it should reach for it rather than answering from its own knowledge or asking a clarifying question instead.
Why the Model Doesn't Just Answer From Training Data
It's worth being explicit about why this mechanism exists rather than simply expecting the model to always know the answer or always ask a clarifying question when it doesn't. A model's training data has a cutoff, and even within that cutoff, it was never trained on your specific, private, or constantly-changing data — your company's current inventory levels, a customer's specific order history, today's currency exchange rate. Function calling is the general-purpose bridge between "things a language model is good at" (understanding a request, deciding what information would help answer it, synthesizing a final response in natural language) and "things a language model cannot inherently do" (accessing live, private, or precisely computed data). This division of labor — the model reasons and decides, your code executes and provides ground truth — is the conceptual core of function calling, and everything in this unit builds on it.
When the Model Chooses Not to Call a Function
Providing tools to a request does not force the model to use them. For a question the model can answer confidently from what it already knows, or a request that doesn't call for any of the provided functions, the model will typically just answer directly, exactly as it would without any tools defined at all.
response = client.responses.create(
model="gpt-5.6-terra",
input="What is the boiling point of water at sea level in Celsius?",
tools=tools, # the get_current_temperature tool from before
)
for item in response.output:
if item.type == "message":
print(item.content[0].text)
elif item.type == "function_call":
print(f"Function call requested: {item.name}")
Here, even though get_current_temperature is available, the model recognizes that the question is about a general physical fact it already knows reliably, not a live temperature reading, and answers directly with a message output item rather than requesting a function call. This is an important behavior to design around: application code that always assumes a response contains a function call (rather than checking the type of each output item, as the example does) will break the moment the model reasonably decides not to use a tool for a given input — checking item.type explicitly, as shown here, is the correct way to handle both possibilities.
Multiple Tools in One Request
A single request can offer the model several tools at once, letting it choose whichever is relevant — or several at once, or none — for a given input.
tools = [
{
"type": "function",
"name": "get_current_temperature",
"description": "Get the current temperature for a given city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
},
{
"type": "function",
"name": "convert_currency",
"description": "Convert an amount of money from one currency to another using current exchange rates.",
"parameters": {
"type": "object",
"properties": {
"amount": {"type": "number"},
"from_currency": {"type": "string", "description": "Three-letter currency code, e.g. 'USD'"},
"to_currency": {"type": "string", "description": "Three-letter currency code, e.g. 'EUR'"},
},
"required": ["amount", "from_currency", "to_currency"],
"additionalProperties": False,
},
},
]
response = client.responses.create(
model="gpt-5.6-terra",
input="How much is 100 US dollars in euros?",
tools=tools,
)
Given this request, the model should recognize convert_currency as the relevant tool and leave get_current_temperature unused — the model is choosing among the available tools based on which one's description matches the intent behind the input, which is why distinct, non-overlapping descriptions across multiple tools matter: two tools with vague or overlapping descriptions make it harder for the model to reliably pick the right one, a topic Lesson 4 returns to when discussing multiple tools in more depth.
Function Calling Versus Structured Outputs
Unit 6 covered structured outputs (text.format with json_schema, or client.responses.parse() with a Pydantic model) as a way to get the model's final answer back in a specific, predictable shape. Function calling looks superficially similar — both involve a JSON Schema, and both produce structured, validated data — but the two solve different problems, and conflating them is a common early confusion worth resolving directly.
| Aspect | Structured Outputs | Function Calling |
|---|---|---|
| Purpose | Shape the model's final answer | Let the model request information or an action mid-conversation |
| Who acts on the schema | Nothing — it's just the final response shape | Your code, which executes the requested function |
| Conversation flow | Single request, single structured response | Request → function call → your code runs it → follow-up request with the result |
| Typical use | Extracting data, classifying content, generating a form-filling answer | Looking up live data, performing calculations, taking an action (sending an email, updating a record) |
A practical rule of thumb: if the goal is "get the model's answer in a predictable shape I can parse," reach for structured outputs (Unit 6). If the goal is "let the model ask for information it doesn't have, or trigger an action outside the model itself," reach for function calling. The two can also be combined — a function's result can itself be used to inform a final structured-output response — but they answer different questions, and it's worth being clear on which one a given problem actually calls for before reaching for either.
Why Argument Schemas Need the Same Discipline as Unit 6's Schemas
The parameters schema in a tool definition is validated the same way Unit 6's json_schema output format is validated: the model is constrained to produce arguments matching the declared types, required fields, and additionalProperties: False restriction, rather than being free to invent an arbitrary argument shape. This matters because a function call whose arguments don't match what your actual Python function expects is a call your code cannot safely execute — if get_current_temperature expects a city string but the model were free to send a nested object or omit the field entirely, the calling code would need defensive parsing logic scattered everywhere a function call is handled, rather than being able to trust that a syntactically valid function call always has arguments in the expected shape.
tools = [
{
"type": "function",
"name": "schedule_meeting",
"description": "Schedule a meeting with a given title, start time, and duration in minutes.",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start_time_iso": {"type": "string", "description": "ISO 8601 datetime, e.g. '2026-09-15T14:00:00'"},
"duration_minutes": {"type": "integer", "minimum": 5, "maximum": 480},
},
"required": ["title", "start_time_iso", "duration_minutes"],
"additionalProperties": False,
},
}
]
response = client.responses.create(
model="gpt-5.6-terra",
input="Set up a 30-minute sync called 'Design Review' for tomorrow at 2pm.",
tools=tools,
)
Adding constraints like minimum and maximum on duration_minutes here does real work beyond documentation: it narrows the space of arguments the model can validly produce, catching an unreasonable value (a meeting scheduled for 50,000 minutes, say) at the schema level rather than requiring schedule_meeting's own implementation to defend against every conceivable bad input the model might otherwise produce. This is the same principle Unit 6, Lesson 2 established for structured-output schemas — push as much validation as reasonably possible into the schema itself, since schema-level constraints are enforced before your code ever sees the arguments, catching a whole class of malformed input for free.
Common Mistakes
Assuming a function_call output item will always be present when tools are provided, rather than checking item.type on each item in response.output, which breaks the moment the model reasonably decides a question doesn't need any tool at all.
Writing a vague or generic function description, leaving the model with too little information to decide reliably whether and when the function is relevant to a given request.
Expecting the model to actually execute the function itself, rather than understanding that a function call is only ever a structured request that application code must interpret and act on.
Confusing function calling with structured outputs because both use JSON Schema, when the two serve genuinely different purposes — shaping a final answer versus requesting information or action mid-conversation.
Best Practices
Write clear, specific function descriptions that state both what the function does and, where relevant, when it should be used, since the description is the model's primary basis for deciding whether to call a function at all.
Always branch on item.type when processing response.output, handling both message and function_call (and any other item types a response might contain) rather than assuming one specific shape.
Keep each function's responsibility narrow and well-defined rather than building one large, multi-purpose function, since a narrowly scoped function with a precise description is easier for the model to select correctly than a broad one that could plausibly apply to many different requests.
Treat function calling and structured outputs as complementary, not interchangeable, choosing the one that matches whether the actual need is "get an answer in a specific shape" or "let the model ask for outside information or trigger an action."