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.