OpenAI Function Calling — Complete Tutorial 2026

Sanjeev SharmaSanjeev Sharma
6 min read

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

  1. You define tools as JSON schemas describing available functions
  2. You send a user message with those tools to the API
  3. The model decides whether to call a tool — if yes, it returns a tool_calls response instead of a text answer
  4. You execute the function and send the result back as a tool role message
  5. 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_calls

Structured 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 args

Setting 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_calls finish 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": true to 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_choice can be "auto", "required", "none", or a specific function — use "required" for guaranteed tool execution
  • Forcing a specific function via tool_choice enables 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading