OpenAI Function Calling — Complete Tutorial 2026
Advertisement
Introduction
Why This Matters
Function calling (also called tool use) transforms GPT-4o from a passive text generator into an active agent that can query databases, call APIs, and trigger application logic. When a user asks "What is the current stock price of AAPL?", the model decides to call a get_stock_price function you defined, receives the result, and incorporates it into the response. This is the foundation of most production AI agents.
How Function Calling Works
- You define tools as JSON schemas describing available functions
- You send a user message with those tools to the API
- The model decides whether to call a tool — if yes, it returns a
tool_callsresponse instead of a text answer - You execute the function and send the result back as a
toolrole message - The model incorporates the result and either answers or calls more tools
The model never directly executes your functions. It only produces a JSON payload describing which function to call and with what arguments. Your code does the execution.
Defining Tools
from openai import OpenAI
import json
client = OpenAI()
# Define tools as JSON schemas
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": (
"Get the current weather for a city. "
"Returns temperature in Celsius and conditions."
),
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'London' or 'New York'",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "search_database",
"description": "Search the product database by name or category.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food"],
},
"max_results": {
"type": "integer",
"default": 10,
"description": "Maximum number of results to return",
},
},
"required": ["query"],
},
},
},
]The Basic Agent Loop
import json
from openai import OpenAI
client = OpenAI()
# Your actual function implementations
def get_weather(city: str, unit: str = "celsius") -> str:
# In production, call a weather API here
return json.dumps({
"city": city,
"temperature": 22 if unit == "celsius" else 72,
"conditions": "Partly cloudy",
"unit": unit,
})
def search_database(query: str, category: str = None, max_results: int = 10) -> str:
# In production, query your database here
return json.dumps({
"results": [
{"id": 1, "name": f"Product matching {query}", "category": category or "general"},
],
"total": 1,
})
TOOL_FUNCTIONS = {
"get_weather": get_weather,
"search_database": search_database,
}
def run_agent(user_message: str) -> str:
"""Run a full agent loop until the model returns a text response."""
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto", # Let the model decide when to call tools
)
choice = response.choices[0]
if choice.finish_reason == "stop":
# Model produced a text answer — we are done
return choice.message.content
if choice.finish_reason == "tool_calls":
# Append assistant message with tool call instructions
messages.append(choice.message)
# Execute each tool call
for tool_call in choice.message.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
if fn_name in TOOL_FUNCTIONS:
result = TOOL_FUNCTIONS[fn_name](**fn_args)
else:
result = json.dumps({"error": f"Unknown function: {fn_name}"})
# Return the result as a tool message
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
else:
raise RuntimeError(f"Unexpected finish_reason: {choice.finish_reason}")
# Test it
print(run_agent("What's the weather in Tokyo and what electronics products do you have?"))Parallel Tool Calls
GPT-4o can call multiple tools in a single response when the tasks are independent. This is efficient — one API call instead of sequential calls.
# The model might return this for "weather in Tokyo and products search"
# Instead of two sequential calls, both come back in one response:
# choice.message.tool_calls = [
# ToolCall(id="call_1", function=Function(name="get_weather", arguments='{"city": "Tokyo"}')),
# ToolCall(id="call_2", function=Function(name="search_database", arguments='{"query": "electronics"}')),
# ]
# Your loop handles both tool calls before calling the API again
# This is automatic — you just process all items in tool_callsStructured Data Extraction
Function calling is excellent for extracting structured data from unstructured text:
tools = [
{
"type": "function",
"function": {
"name": "extract_invoice_data",
"description": "Extract structured invoice data from text",
"parameters": {
"type": "object",
"properties": {
"vendor_name": {"type": "string"},
"invoice_number": {"type": "string"},
"total_amount": {"type": "number"},
"currency": {"type": "string"},
"due_date": {
"type": "string",
"description": "ISO 8601 date format: YYYY-MM-DD",
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"},
},
},
},
},
"required": ["vendor_name", "total_amount", "currency"],
},
},
}
]
def extract_invoice(raw_text: str) -> dict:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Extract data from this invoice:\n\n{raw_text}"}],
tools=tools,
tool_choice={"type": "function", "function": {"name": "extract_invoice_data"}},
)
args = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
return argsSetting tool_choice to a specific function forces the model to always call that function — useful for extraction tasks where you always want structured output.
Common Mistakes
- Not handling the
tool_callsfinish reason — your loop breaks and returns the wrong message - Forgetting to append the assistant message before tool result messages — the API requires the full sequence
- Not validating function arguments before execution — the model can pass unexpected values
- Using function calling for simple tasks — a direct prompt with JSON mode is cheaper and faster for extraction
- Not handling unknown function names — if the model hallucinates a function name, your code crashes
Best Practices
- Keep tool descriptions precise — the model reads them to decide when and whether to call each tool
- Add
"strict": trueto function definitions to enforce the exact schema (available in newer API versions) - Use
tool_choice="required"when the task always needs a tool call to avoid text-only responses - Validate and sanitize function arguments before executing, especially for database queries or file operations
- Log all tool calls and results — they are essential for debugging agent behavior in production
Key Takeaways
- Function calling lets GPT-4o trigger your application logic by returning a structured JSON payload — your code executes the function
- The model never directly runs your functions; it only produces the call specification
- Parallel tool calls let the model invoke multiple functions in one API response for independent tasks
tool_choicecan be"auto","required","none", or a specific function — use"required"for guaranteed tool execution- Forcing a specific function via
tool_choiceenables reliable structured data extraction from unstructured text - Always validate and sanitize function arguments before execution — especially for SQL queries and file paths
- The messages array must include: user message → assistant tool_calls message → tool result messages → next API call
- Function calling costs the same as regular completions — the tool definitions count as input tokens
Advertisement