Function calling patterns.

Schema with Pydantic

from pydantic import BaseModel, Field

class SearchArgs(BaseModel):
    query: str = Field(description="Search query")
    limit: int = Field(default=10, ge=1, le=100)

tool = {
    "name": "search",
    "description": "Search the web",
    "input_schema": SearchArgs.model_json_schema(),
}

Validation

def run_tool(name: str, args: dict):
    if name == "search":
        try:
            validated = SearchArgs(**args)
        except ValidationError as e:
            return {"error": str(e)}
        return do_search(validated.query, validated.limit)
    return {"error": "unknown tool"}

Tool registry

TOOLS = {}

def tool(name=None, description=None):
    def dec(fn):
        TOOLS[name or fn.__name__] = {"fn": fn, "description": description or fn.__doc__}
        return fn
    return dec

@tool(description="Get weather")
def get_weather(city: str, unit: str = "c") -> dict:
    return {"temp": 20, "unit": unit}

Auto-generate schemas with pydantic or instructor.

Multi-tool parallel

LLM can call multiple in one response:

for block in response.content:
    if block.type == "tool_use":
        results.append(asyncio.create_task(run_tool(block.name, block.input)))

results = await asyncio.gather(*results)

Structured output via tool

Force structured JSON:

class Result(BaseModel):
    summary: str
    tags: list[str]

tools = [{"name": "submit", "description": "Submit result", "input_schema": Result.model_json_schema()}]
tool_choice = {"type": "tool", "name": "submit"}

Tools as instances

class Tools:
    def __init__(self, db): self.db = db
    
    def fetch_user(self, id: int):
        return self.db.user.find(id)

tools_obj = Tools(db)
def run(name, args):
    return getattr(tools_obj, name)(**args)

Error → recovery

try:
    result = tool(**args)
except SomeError as e:
    result = {"error": str(e), "hint": "Try with different args"}

# Pass back; LLM may retry

instructor library (Pydantic-first)

import instructor
from openai import OpenAI

client = instructor.from_openai(OpenAI())

result = client.chat.completions.create(
    model="gpt-5",
    response_model=Result,
    messages=[...],
)
# result is a validated Result instance

Retry built-in on validation failure.

Common mistakes

  • Tools accepting any JSON without schema → garbage in.
  • Long-running tool → request timeout.
  • Returning huge dict → fills context.
  • Tools that have side effects in test runs.
  • Missing return value handler → LLM stuck.

Read this next

If you want my function-calling helpers, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .