Function calling lets a language model request execution of your code instead of guessing answers. This langchain python function calling walkthrough builds a minimal tool-calling loop from scratch, using ChatOpenAI and the LangChain core tool interface, so you understand every message passed between your app and the model.
Prerequisites
You need Python 3.10 or newer. Install the minimal set of packages:
pip install langchain-openai langchain-core openai
You also need an API key from an OpenAI-compatible provider. If you want to avoid hard-coupled provider dependencies, an OpenAI-compatible gateway like n4n.ai exposes one endpoint for 240+ models and forwards cache-control hints, so the same ChatOpenAI instantiation works by swapping base_url. The rest of this walkthrough uses the standard OpenAI endpoint; change base_url if you use a gateway.
Set your key in the environment:
export OPENAI_API_KEY="sk-..."
Define your tools
LangChain treats a tool as a callable with a schema. The fastest way is the @tool decorator from langchain_core.tools. The docstring becomes the tool description the model sees, so write it precisely.
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Return the current temperature in Fahrenheit for a given city."""
# Mock implementation — replace with a real API call.
mock_data = {"boston": "72F", "sf": "65F", "nyc": "80F"}
return mock_data.get(city.lower(), "unknown")
@tool
def calculate(expression: str) -> str:
"""Evaluate a basic arithmetic expression and return the result."""
try:
# Restrict eval to safe arithmetic only.
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as e:
return f"error: {e}"
Each decorated function is now a BaseTool instance. You can inspect its schema:
print(get_weather.args)
# {'city': {'title': 'City', 'type': 'string'}}
Bind tools to the model
Create a chat model and attach the tools with bind_tools. This injects the tool schemas into the request payload sent to the provider.
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools([get_weather, calculate])
The returned object is still a runnable chat model; it just knows about your tools.
Invoke and inspect the tool call
Send a prompt that clearly requires a tool. The model responds with an AIMessage that may contain tool_calls.
response = model_with_tools.invoke("What is 24 * 7 and the weather in Boston?")
print(response.content)
print(response.tool_calls)
Expected output (IDs will vary):
(None or empty string)
[{'name': 'calculate', 'args': {'expression': '24 * 7'}, 'id': 'call_abc'}, {'name': 'get_weather', 'args': {'city': 'Boston'}, 'id': 'call_def'}]
If response.tool_calls is empty, the model answered directly. In a production loop, always check for tool_calls before executing.
Execute the call and feed results back
You must run the requested tools and return their output as ToolMessage objects, referencing the tool_call_id. The model uses those results to produce a final answer.
from langchain_core.messages import HumanMessage, ToolMessage
messages = [HumanMessage(content="What is 24 * 7 and the weather in Boston?")]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)
for call in ai_msg.tool_calls:
if call["name"] == "calculate":
result = calculate.invoke(call["args"])
elif call["name"] == "get_weather":
result = get_weather.invoke(call["args"])
else:
result = "unknown tool"
messages.append(ToolMessage(content=result, tool_call_id=call["id"]))
final = model_with_tools.invoke(messages)
print(final.content)
Expected output:
24 * 7 is 168, and the current temperature in Boston is 72F.
That is the full round trip: model requests tools, your code executes them, model synthesizes the answer.
Handling multiple tools and errors
Real tools fail. Wrap execution so a single bad call doesn’t kill the loop, and let the model recover.
def execute_tool_call(call: dict) -> ToolMessage:
name = call["name"]
try:
if name == "calculate":
return ToolMessage(content=calculate.invoke(call["args"]), tool_call_id=call["id"])
if name == "get_weather":
return ToolMessage(content=get_weather.invoke(call["args"]), tool_call_id=call["id"])
except Exception as e:
return ToolMessage(content=f"tool error: {e}", tool_call_id=call["id"])
return ToolMessage(content="unknown tool", tool_call_id=call["id"])
If a tool returns an error string, the model can retry or explain the failure. For example, if calculate gets "import os", the sandbox eval raises and returns "error: ...", which the model can report instead of executing arbitrary code.
Streaming tool calls
For latency-sensitive apps, stream the model output. Tool calls arrive in the final chunk’s tool_calls field, not as tokens.
stream = model_with_tools.stream("Weather in SF?")
tool_calls = []
for chunk in stream:
if chunk.tool_calls:
tool_calls.extend(chunk.tool_calls)
print(tool_calls)
You still execute them the same way after the stream completes.
Production notes
Validate arguments. The model can send malformed args. Use Pydantic models instead of raw dicts for strict validation:
from pydantic import BaseModel, Field
class WeatherInput(BaseModel):
city: str = Field(description="City name, e.g. Boston")
@tool("get_weather", args_schema=WeatherInput)
def get_weather(city: str) -> str:
"""Return temperature for a city."""
return mock_data.get(city.lower(), "unknown")
Set timeouts. Network tools need hard limits. Wrap external calls with asyncio.wait_for or requests timeouts.
Idempotency. Tool execution may be retried by the model. Make tools safe to call twice (e.g., read-only queries, or dedupe on tool_call_id).
Billing and routing. When you bind many tools, the schema inflates the prompt. Track token usage via the usage_metadata field on the response if your provider meters per token. Gateways that honor client routing directives let you pin specific models per tool class without changing code.
Structured output vs tools. If you only need the model to return JSON, use with_structured_output instead of function calling. Reserve function calling for actions your code must perform.
Full minimal script
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
@tool
def get_weather(city: str) -> str:
"""Return temperature in Fahrenheit for a city."""
return {"boston": "72F"}.get(city.lower(), "unknown")
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools([get_weather])
messages = [HumanMessage(content="Weather in Boston?")]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)
for call in ai_msg.tool_calls:
result = get_weather.invoke(call["args"])
messages.append(ToolMessage(content=result, tool_call_id=call["id"]))
print(model_with_tools.invoke(messages).content)
Run it. You should see The current temperature in Boston is 72F. (wording may vary). That confirms your langchain python function calling walkthrough is wired correctly. From here, swap the mock for real APIs, add error boundaries, and ship.