Function calling lets LLMs return structured data and trigger actions instead of just generating text. It's the foundation of every serious LLM application.
How Function Calling Works
You define a schema, the model returns a structured JSON object matching that schema, and your code executes the actual function. The model never runs code -- it just signals intent.
OpenAI Function Calling
from openai import OpenAI
import json
client = OpenAI()
functions = [
{
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City and country, e.g. 'Vancouver, CA'"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
]
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
tools=[{"type": "function", "function": f} for f in functions],
tool_choice="auto"
)
msg = response.choices[0].message
if msg.tool_calls:
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = get_weather(**args)
print(f"Tool: {call.function.name}, Result: {result}")
Claude Tool Use
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
]
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)
for block in response.content:
if block.type == "tool_use":
result = get_weather(**block.input)
# Return result in next message turn
Parallel Function Calls
Both APIs support calling multiple functions simultaneously:
# Handle all tool calls before responding
tool_results = []
for call in response.choices[0].message.tool_calls:
args = json.loads(call.function.arguments)
result = dispatch_tool(call.function.name, args)
tool_results.append({
"tool_call_id": call.id,
"role": "tool",
"content": json.dumps(result)
})
follow_up = client.chat.completions.create(
model="gpt-4o",
messages=[original_message, response.choices[0].message, *tool_results]
)
Writing Good Function Descriptions
The description is what the model reads to decide when to call your function. Bad descriptions lead to wrong calls.
Bad:
{"name": "db_query", "description": "Query the database"}
Good:
{
"name": "search_products",
"description": "Search the product catalog by name, category, or price range. Use this when the user asks about available products, wants to find something specific, or asks what you carry. Returns up to 10 matching products with prices and inventory status."
}
Error Handling
Functions fail. Handle it gracefully:
def safe_tool_call(name: str, args: dict) -> str:
try:
result = dispatch_tool(name, args)
return json.dumps(result)
except ValueError as e:
return json.dumps({"error": f"Invalid input: {e}"})
except TimeoutError:
return json.dumps({"error": "Tool timed out. Try a simpler query."})
except Exception as e:
return json.dumps({"error": f"Tool failed: {type(e).__name__}"})
Always return something to the model -- never raise an exception that cuts the conversation short.
Structured Outputs Alternative
For extraction (not action), consider structured outputs:
from pydantic import BaseModel
class ProductInfo(BaseModel):
name: str
price: float
in_stock: bool
response = client.beta.chat.completions.parse(
model="gpt-4o",
messages=messages,
response_format=ProductInfo
)
product = response.choices[0].message.parsed
print(product.name, product.price)
Function calling is for triggering actions. Structured outputs are for extracting information. Use the right tool for the job.