n4nAI

Building a weather tool with OpenAI function calling

Step-by-step Python tutorial for a weather tool using OpenAI function calling: define schema, call API, execute function, and stream responses.

n4n Team2 min read451 words

Audio narration

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

Building a weather tool with OpenAI function calling in Python lets you bridge natural language queries to live API data without hand-rolling prompt parsers. This tutorial ships a runnable script that defines a function schema, dispatches the model’s call, fetches real weather from a free API, and returns a clean answer.

Prerequisites

You need Python 3.10+ and a working OpenAI API key. The weather data comes from Open-Meteo, which requires no API key.

python -m venv .venv
source .venv/bin/activate
pip install openai requests
export OPENAI_API_KEY="sk-..."

You should be comfortable with requests and the OpenAI Python SDK v1.x. The full script at the end runs end to end.

Define the function schema

OpenAI function calling expects a JSON Schema describing your tool. The model uses it to emit structured arguments, not free text. Keep the schema tight—only what your function actually accepts.

{
  "type": "function",
  "function": {
    "name": "get_current_weather",
    "description": "Get current temperature and wind for a location",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "City name, e.g. 'San Francisco'"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "Temperature unit"
        }
      },
      "required": ["location", "unit"]
    }
  }
}

The description fields matter. The model routes on them, so write them like docs for a teammate.

Implement the local weather function

We resolve the city to coordinates via Open-Meteo’s geocoding endpoint, then pull current weather. This keeps the tutorial key-free aside from OpenAI.

import requests

def get_current_weather(location: str, unit: str) -> dict:
    geo = requests.get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params={"name": location, "count": 1},
        timeout=10,
    ).json()
    if not geo.get("results"):
        return {"error": f"Location '{location}' not found"}
    lat = geo["results"][0]["latitude"]
    lon = geo["results"][0]["longitude"]

    weather = requests.get(
        "https://api.open-meteo.com/v1/forecast",
        params={"latitude": lat, "longitude": lon, "current_weather": True},
        timeout=10,
    ).json()

    temp = weather["current_weather"]["temperature"]
    if unit == "fahrenheit":
        temp = temp * 9 / 5 + 32
    return {
        "location": geo["results"][0]["name"],
        "temperature": round(temp, 1),
        "unit": unit,
        "wind_speed": weather["current_weather"]["windspeed"],
    }

The function returns a plain dict. Serialize it to JSON when sending back to the model.

Wire up the OpenAI call

Create the client and send the user question with the tool attached. Use tool_choice="auto" so the model decides whether to call.

from openai import OpenAI
import json

client = OpenAI()
tools = [{
    "type": "function",
    "function": {
        "name": "get_current_weather",
        "description": "Get current temperature and wind for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location", "unit"]
        }
    }
}]

messages = [{"role": "user", "content": "What's the weather in Tokyo in celsius?"}]
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 the tool, msg.tool_calls is populated.

Execute and respond

Detect the call, run the local function, and append the result as a tool message. Then call the model again to synthesize the final answer.

if msg.tool_calls:
    call = msg.tool_calls[0]
    args = json.loads(call.arguments)
    result = get_current_weather(**args)

    messages.append(msg)
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result),
    })
    final = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
    )
    print(final.choices[0].message.content)
else:
    print(msg.content)

The second call returns natural language. You never parse the weather JSON in your app code—the model does.

Checkpoint: expected outputs

After the first call, msg.tool_calls looks like:

[
  {
    "id": "call_abc123",
    "type": "function",
    "function": {
      "name": "get_current_weather",
      "arguments": "{\"location\":\"Tokyo\",\"unit\":\"celsius\"}"
    }
  }
]

The local function returns:

{"location": "Tokyo", "temperature": 22.4, "unit": "celsius", "wind_speed": 12.0}

The final printed response is similar to:

The current weather in Tokyo is 22.4°C with a wind speed of 12 km/h.

If you skip the tool round-trip, you’ll get hallucinated numbers. The two-call pattern is mandatory for live data.

Error handling and validation

Production code must guard against three failures: network timeouts in get_current_weather, missing results from geocoding, and the model emitting invalid enum values. Wrap the function in try/except and return an error dict the model can explain.

try:
    result = get_current_weather(**args)
except Exception as e:
    result = {"error": str(e)}

Also validate unit before the API call—don’t trust the model’s JSON blindly. Pydantic works well here, but a manual check is fine for a tutorial.

Using a gateway for resilience

If you route through an OpenAI-compatible gateway such as n4n.ai, the exact request shape above works unchanged and you get automatic fallback when a provider is rate-limited. The tools parameter and tool_calls response are forwarded as-is. That’s useful when you scale beyond a single provider and still want function calling to behave identically.

Full runnable script

import json
import requests
from openai import OpenAI

client = OpenAI()

def get_current_weather(location: str, unit: str) -> dict:
    geo = requests.get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params={"name": location, "count": 1},
        timeout=10,
    ).json()
    if not geo.get("results"):
        return {"error": f"Location '{location}' not found"}
    lat = geo["results"][0]["latitude"]
    lon = geo["results"][0]["longitude"]
    weather = requests.get(
        "https://api.open-meteo.com/v1/forecast",
        params={"latitude": lat, "longitude": lon, "current_weather": True},
        timeout=10,
    ).json()
    temp = weather["current_weather"]["temperature"]
    if unit == "fahrenheit":
        temp = temp * 9 / 5 + 32
    return {
        "location": geo["results"][0]["name"],
        "temperature": round(temp, 1),
        "unit": unit,
        "wind_speed": weather["current_weather"]["windspeed"],
    }

tools = [{
    "type": "function",
    "function": {
        "name": "get_current_weather",
        "description": "Get current temperature and wind for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location", "unit"]
        }
    }
}]

messages = [{"role": "user", "content": "What's the weather in Tokyo in celsius?"}]
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)
msg = resp.choices[0].message

if msg.tool_calls:
    call = msg.tool_calls[0]
    args = json.loads(call.arguments)
    result = get_current_weather(**args)
    messages.append(msg)
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result),
    })
    final = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
    print(final.choices[0].message.content)
else:
    print(msg.content)

Run it with python weather_tool.py. The weather tool openai function calling python pattern scales to any external API—swap get_current_weather for your own and keep the dispatch loop identical.

Tagspythonopenaifunction-callingtutorial

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 function calling in python posts →