n4nAI

Function calling with the OpenAI Python SDK: a full guide

A practical guide to openai python sdk function calling: define tools, handle tool calls, run loops, stream, and avoid schema and latency pitfalls.

n4n Team3 min read572 words

Audio narration

Coming soon — every post will get a voice note here.

Function calling with the openai python sdk function calling turns a language model from a text generator into a structured action dispatcher. The pattern is stable across providers that expose the OpenAI-compatible chat completions interface: you declare callable tools, the model emits arguments as JSON, and your code executes the side effect.

Install and point the client

The official SDK is openai. Pin a recent major version; the tools parameter has been stable since 1.0.

pip install "openai>=1.0"
from openai import OpenAI

client = OpenAI()  # reads OPENAI_API_KEY from env

If you proxy through a gateway, set base_url. The rest of this guide is identical because the request shape is the contract.

Define tools as JSON Schema

A tool is a dictionary with type: "function" and a function block. The parameters field is a JSON Schema object. Keep it tight—every key is sent on every request.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Current temperature for a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string"},
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                },
                "required": ["location"]
            }
        }
    }
]

Avoid verbose descriptions. The model reads them, but so does your context window and your bill.

Send the first request

Set tools and let the model decide with tool_choice="auto". Use "none" to suppress calls, or force a specific tool with {"type":"function","function":{"name":"get_weather"}}.

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

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

msg = resp.choices[0].message

If the model wants to call a tool, msg.tool_calls is a list. Each item has id, function.name, and function.arguments (a JSON string).

Execute and feed results back

You must append the assistant message (with tool_calls) to the conversation before appending the tool result. Skip this and the API rejects the request.

import json

def get_weather(location: str, unit: str = "celsius") -> dict:
    # stub: real impl hits a weather API
    return {"temp": 22, "unit": unit}

if msg.tool_calls:
    messages.append(msg)  # preserve the assistant turn
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result)
        })

The role: "tool" message requires tool_call_id matching the call. Content must be a string—serialize dicts yourself.

Build the agent loop

One call rarely finishes the task. Wrap the exchange in a bounded loop.

MAX_STEPS = 5
for _ in range(MAX_STEPS):
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        tools=tools
    )
    msg = resp.choices[0].message
    if not msg.tool_calls:
        print(msg.content)
        break
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result)
        })
else:
    raise RuntimeError("exceeded MAX_STEPS")

Always cap iterations. Models will happily call get_weather in a circle if your stub returns the same data.

Streaming tool calls

Streaming works, but tool_calls arrive as deltas. Accumulate function.arguments per id before parsing.

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=tools,
    stream=True
)

buffer = {}
for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.tool_calls:
        for tc in delta.tool_calls:
            buf = buffer.setdefault(tc.id, {"name": "", "args": ""})
            if tc.function.name:
                buf["name"] += tc.function.name
            if tc.function.arguments:
                buf["args"] += tc.function.arguments

After the stream ends, parse each buf["args"] with json.loads. Do not assume the JSON is complete mid-stream.

Common pitfalls and tradeoffs

Schema drift and strict mode

Models occasionally emit keys not in your schema. If your backend uses Pydantic, validate after parsing and return a tool error message rather than crashing. Some providers support strict tool schemas that constrain the model to the exact shape; enable it when available.

Parallel calls and partial failure

A single assistant message can contain multiple tool_calls. Execute them concurrently, but append each result with its own tool_call_id. If one fails, return an error string for that id—the model can often recover.

# pseudo-concurrent
results = asyncio.gather(*[run_call(c) for c in msg.tool_calls])

Token overhead and latency

Every tool definition is resent each turn. Five tools with rich descriptions can add 300–800 tokens per request. Trim descriptions, share schemas across calls, and drop unused tools dynamically based on conversation state.

Routing and resilience

If you front requests with an OpenAI-compatible endpoint such as n4n.ai, automatic fallback when a provider is rate-limited or degraded keeps the agent loop running, and per-token usage metering exposes how much your schemas cost. The SDK code above does not change; just set base_url. Still, ensure your tool schemas are portable—not every model supports the same strictness flags.

Production checklist

  • Validate arguments with a schema loader before function execution.
  • Never trust tool_call_id uniqueness across separate requests; scope it to the conversation.
  • Log the full messages array on failure; the assistant tool_calls turn is the usual missing piece.
  • Set temperature=0 for deterministic tool selection when the task is mechanical.
  • Cache tool definitions in memory; rebuilding them per call wastes cycles.

Function calling with the openai python sdk function calling is mostly disciplined bookkeeping. Get the message order right, bound the loop, and treat the schema as paid payload. The rest is application logic.

Tagspythonopenai-sdkfunction-callingtool-use

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All python + openai-compatible sdk integration posts →