LangChain Tutorial (Python): building custom tools for intermediate developers
This tutorial shows how to build custom LangChain tools in Python, wire them into an agent, and test that the agent can call your tool correctly. You need this when the built-in tools are not enough and you want your agent to hit internal APIs, enforce business rules, or wrap deterministic Python logic.
What You'll Need
- •Python 3.10+
- •
langchain - •
langchain-openai - •
pydantic - •An OpenAI API key in
OPENAI_API_KEY - •Basic familiarity with LangChain agents and chat models
- •A terminal and a virtual environment
Install the packages:
pip install langchain langchain-openai pydantic
Step-by-Step
- •Start by defining a real tool function. Keep the logic deterministic and side-effect free at first, because that makes debugging much easier than starting with network calls or database writes.
from typing import Annotated
from pydantic import BaseModel, Field
class TaxInput(BaseModel):
amount: float = Field(..., gt=0, description="Pre-tax amount in USD")
state: str = Field(..., min_length=2, max_length=2, description="US state code")
def calculate_sales_tax(amount: float, state: str) -> str:
rates = {"CA": 0.0725, "NY": 0.04, "TX": 0.0625}
rate = rates.get(state.upper(), 0.05)
tax = round(amount * rate, 2)
total = round(amount + tax, 2)
return f"state={state.upper()}, rate={rate}, tax={tax}, total={total}"
- •Wrap that function as a LangChain tool using
@tool. This gives the agent a name, description, and argument schema it can use when deciding whether to call it.
from langchain_core.tools import tool
@tool
def sales_tax_tool(amount: float, state: str) -> str:
"""Calculate sales tax for a US state code."""
return calculate_sales_tax(amount=amount, state=state)
print(sales_tax_tool.name)
print(sales_tax_tool.description)
- •If you need stricter validation, define a structured input model and bind it to the tool with explicit args schema behavior. This is the pattern you want for production tools because it keeps bad inputs from leaking into your business logic.
from langchain_core.tools import StructuredTool
def calculate_invoice_total(amount: float, discount_pct: float) -> str:
discount = round(amount * (discount_pct / 100), 2)
total = round(amount - discount, 2)
return f"amount={amount}, discount={discount}, total={total}"
invoice_tool = StructuredTool.from_function(
func=calculate_invoice_total,
name="invoice_total_tool",
description="Calculate discounted invoice totals.",
)
print(invoice_tool.invoke({"amount": 250.0, "discount_pct": 10}))
- •Now plug your custom tool into an agent. The key point is that the model does not execute Python directly; LangChain routes tool calls through the agent loop.
import os
from langchain_openai import ChatOpenAI
from langchain.agents import initialize_agent, AgentType
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [sales_tax_tool]
agent = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True,
)
response = agent.invoke({"input": "What is the sales tax on $120 in CA?"})
print(response["output"])
- •Add a second tool for a common intermediate pattern: fetching data from an internal source before doing reasoning. In real systems this might be a policy service, CRM lookup, or pricing endpoint; here we keep it local so the example runs as-is.
CUSTOMER_DB = {
"1001": {"name": "Amina", "segment": "premium"},
"1002": {"name": "Jordan", "segment": "standard"},
}
@tool
def lookup_customer(customer_id: str) -> str:
"""Lookup customer details by customer ID."""
customer = CUSTOMER_DB.get(customer_id)
if not customer:
return f"customer_id={customer_id}, found=False"
return f"customer_id={customer_id}, found=True, name={customer['name']}, segment={customer['segment']}"
tools.append(lookup_customer)
agent = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True,
)
print(agent.invoke({"input": "Look up customer 1001 and tell me their segment."})["output"])
Testing It
Run each code block in order and confirm that the standalone tool functions return strings before you involve the agent. Then run the agent with simple prompts like “What is the sales tax on $120 in CA?” and check that verbose=True shows an actual tool call instead of a hallucinated answer.
If the model ignores your tool, make sure you are using a function-calling capable chat model and that the tool description clearly matches the user request. If validation fails, tighten your argument schema and let Pydantic reject bad inputs early.
A good test is to ask for both supported and unsupported cases:
- •Supported:
What is the sales tax on $120 in CA? - •Unsupported:
What is the sales tax on $120 in ZZ?
You should see deterministic output for both cases, with fallback behavior for unknown states.
Next Steps
- •Replace local dictionaries with real API clients using retry logic and timeouts.
- •Move from string-returning tools to structured outputs with Pydantic models.
- •Learn how to compose tools with LangGraph when you need multi-step workflows and stateful orchestration
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit