6Tool use with Claude

Tool functions

5 min read969 words

A tool, stripped of all the JSON schema machinery and message block ceremony around it, is just a function. A regular Python function. When you say "I gave Claude a tool," what you actually did was write a function on your server and told Claude it exists. This lesson is about writing that function well — before you touch a single piece of Claude-specific wiring.

What a tool function actually is

A tool function is a plain Python function that your server runs when Claude asks it to. That's it. No decorators, no special base class, no registration with an AI framework. If you can write the function as a normal part of your codebase, you can use it as a tool.

The only thing that makes a function "a tool" is that somewhere else in your code, you've described it to Claude via a schema and wired up the loop that lets Claude request it. The function itself is ordinary Python.

Best practices that pay off specifically because Claude is calling it

A few rules that are always good hygiene become load-bearing when a language model is on the other end of the call:

Use descriptive names. Function names and parameter names are not just documentation — they're signal to Claude about what the tool does. get_current_datetime(date_format) is obvious. gcdt(fmt) is a riddle. The model will do better with names it can read.

Validate inputs, and raise clear errors when they fail. Claude sees your error messages. If you raise ValueError("date_format cannot be empty"), Claude can read that, understand what went wrong, and retry the call with a valid argument. If you silently return None or an empty string, Claude has nothing to learn from and the whole flow breaks quietly. Loud errors are better than quiet failures because the language model on the other side can actually respond to them.

Provide meaningful error messages. "Invalid argument" is useless. "date_format must be a valid Python strftime format string; received empty string" is actionable. Write error messages as if the reader is going to use them to fix the problem — because that reader is now sometimes a language model.

Prefer raising to returning sentinel values. raise ValueError("...") surfaces cleanly through the tool use protocol as a tool error. A returned None or {} looks like a valid result to Claude, which will then confidently tell the user the answer based on no data at all.

Your first tool function: get_current_datetime

The first tool in the reminder project is the simplest. It returns the current date and time, formatted according to a caller-supplied format string:

from datetime import datetime

def get_current_datetime(date_format="%Y-%m-%d %H:%M:%S"):
    if not date_format:
        raise ValueError("date_format cannot be empty")
    return datetime.now().strftime(date_format)

Three things worth noting:

  • Default argument. If Claude doesn't supply a format, we default to ISO-ish YYYY-MM-DD HH:MM:SS. Sensible defaults are another signal — they reduce the cases where Claude has to guess.
  • Input validation. The if not date_format check catches empty strings and None before they reach strftime. We raise a clear ValueError so Claude can recover from its own mistake by retrying with a real format string.
  • Deterministic output. Given the same inputs, this function returns essentially the same shape every time. That matters — tools should be predictable, so Claude can rely on their behaviour across calls.

You can test it in isolation before you ever introduce it to Claude:

>>> get_current_datetime()
'2024-05-22 14:30:25'

>>> get_current_datetime("%H:%M")
'14:30'

>>> get_current_datetime("")
ValueError: date_format cannot be empty

All three cases behave sensibly — the happy path, the customised path, and the error path. A good tool function is one you could plausibly use from regular code, not just from a tool-calling loop. If the function wouldn't survive code review on its own merits, it won't help Claude either.

Why the validation matters even for unlikely errors

You might look at if not date_format and think: "Claude would never pass an empty string, this check is pointless." It's not. Two reasons:

  • Claude sometimes does weird things. Not often, but often enough. A validation layer protects you regardless.
  • More importantly, the pattern matters. Get into the habit of validating every tool argument and raising informative errors on every failure. When you build a more complex tool later — one where the input space is actually messy — you'll want the muscle memory of "write the validation first, then the happy path."

What's next

Writing the function is step one of three. Claude can't call this function yet because Claude doesn't know it exists. The next step is to write a tool schema — a structured description of the function that Claude can read — and pass it along with the messages when you call the API. That's the next lesson. The one after that deals with the multi-block message shape Claude uses to actually request the call.

Key Takeaways

  • 1A tool function is just a plain Python function your server runs when Claude requests it — no decorators or special framework integration required.
  • 2Descriptive function and parameter names are signal to Claude, not just documentation — clear names improve tool-use accuracy.
  • 3Validate inputs and raise informative errors on failure; Claude reads error messages and can retry with corrected arguments, but only if the error is actually informative.
  • 4Loud errors beat quiet failures — raising a ValueError is better than silently returning None because Claude can recover from the raise but not from the silence.
  • 5get_current_datetime is the reminder project's simplest tool: a one-line function with a default format, input validation, and deterministic output.
  • 6Build the habit of writing tool functions that would survive code review on their own merits — a well-written function is a well-written tool.