OpenAI function calling (tools API): complete Python guide

OpenAI's function calling — now called the tools API — lets you describe functions to the model, have it decide when to call them, and then integrate the results into its final reply. The old functions parameter is deprecated; all new code should use tools. This guide covers tool definitions, the full call loop with the Python SDK v1.x, tool_choice, parallel calls, strict mode, and a side-by-side comparison with Claude tool use.

The 30-second answer

How to define a tool

Each tool entry wraps a function definition. The description guides the model on when to call it — write it from the model's perspective, describing the conditions under which it should reach for this tool:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name, e.g. 'London'"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"]
                    }
                },
                "required": ["city"]
            }
        }
    }
]

The parameters field uses standard JSON Schema. Mark required arguments in "required". Optional parameters (like unit above) may or may not be populated by the model. You can include multiple tools in the list — the model picks based on descriptions. Note the nesting: the JSON Schema goes inside "function", which goes inside the top-level tool object.

The complete call loop

This is the full working example using the OpenAI Python SDK v1.x:

from openai import OpenAI
import json

client = OpenAI()

def get_weather(city: str, unit: str = "celsius") -> str:
    # Your actual implementation here
    return json.dumps({"city": city, "temp": 22, "unit": unit})

messages = [{"role": "user", "content": "What's the weather in Paris?"}]

# Step 1: Send with tools
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

# Step 2: Check for tool call
message = response.choices[0].message
if message.tool_calls:
    # Step 3: Append the assistant message (with the tool calls)
    messages.append(message)

    # Step 4: Execute each tool call and return results
    for tool_call in message.tool_calls:
        args = json.loads(tool_call.function.arguments)
        result = get_weather(**args)

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result
        })

    # Step 5: Get the final response
    final = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools
    )
    print(final.choices[0].message.content)

A few things to note: always append the assistant's message object before appending tool results — the conversation history must include the assistant turn that contained the tool calls. Each tool result message must include tool_call_id matching the id from the tool call. The content field of the result message is a string — JSON-serialise structured data before passing it in.

Controlling tool use with tool_choice

Four options, from fully automatic to fully suppressed:

# Model decides whether to call a tool (default)
tool_choice = "auto"

# Model must call at least one tool
tool_choice = "required"

# Force a specific tool
tool_choice = {"type": "function", "function": {"name": "get_weather"}}

# Disable tool use entirely for this call
tool_choice = "none"

# Pass it in the create() call:
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="required"
)

Use "required" when building agentic loops that must always take an action. Use the specific function form when using tool calling as a structured-output mechanism — force the model to always populate your schema. Use "none" to temporarily disable tools without removing them from the request (useful if you want to keep the tool definitions available but bypass them for a specific turn).

Parallel tool calls and strict mode

The model can return multiple tool calls in one response. message.tool_calls is always a list — loop through all entries:

if message.tool_calls:
    messages.append(message)

    for tool_call in message.tool_calls:
        args = json.loads(tool_call.function.arguments)

        # Dispatch to the right function
        if tool_call.function.name == "get_weather":
            result = get_weather(**args)
        elif tool_call.function.name == "get_time":
            result = get_time(**args)
        else:
            result = json.dumps({"error": f"Unknown function: {tool_call.function.name}"})

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result
        })

    # All results must be in the message list before the final call
    final = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools
    )

Strict mode locks the model's output to your exact schema — useful when you're parsing the arguments programmatically and can't tolerate schema drift. Enable it by adding "strict": true inside the "function" object, and add "additionalProperties": false to every object type in the schema:

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather for a city.",
        "strict": true,
        "parameters": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["city", "unit"]
        }
    }
}

Note that in strict mode, all properties must be listed in required — the model cannot omit fields. If you want an optional parameter, model it as a union with null (e.g. "type": ["string", "null"]) and include it in required. Strict mode may add a small amount of latency on the first call while the schema is processed.

How this differs from Claude tool use

The loop logic is nearly identical. If you've used one, you can use the other. The differences are in field names and structure:

AspectOpenAIClaude
Tool call signalfinish_reason: "tool_calls"stop_reason: "tool_use"
Result role"role": "tool""type": "tool_result" content block
Tool definitionNested: type/function/parametersFlat: name/description/input_schema
Disable toolstool_choice: "none"Omit the tools parameter
Require any tooltool_choice: "required"tool_choice: {"type": "any"}
Strict schema"strict": true in function defNot available (JSON Schema validation only)

FAQ

Is OpenAI function calling the same as the tools API? Yes — the original functions parameter from 2023 is deprecated. All new code should use the tools parameter with "type": "function" entries. The functionality is identical; the new format adds support for future non-function tool types.

What does strict mode do? Setting "strict": true forces the model's output to exactly match your JSON schema — no extra fields, no missing required fields. It requires "additionalProperties": false on all objects and all properties in required. Use it for production workflows where you need predictable, parseable output.

How is this different from Claude tool use? The loop logic is nearly identical. Key differences: OpenAI uses role: "tool" for results; Claude uses a tool_result content block. OpenAI's finish_reason is "tool_calls"; Claude's stop_reason is "tool_use". OpenAI has a "none" tool_choice option; Claude does not.

Last updated May 28, 2026. Code examples verified against the OpenAI Python SDK v1.x and OpenAI function calling documentation. API behaviour may change — confirm against the official docs before deploying to production.