In Google ADK, how does a plain Python function become a callable tool?
answer
- No tool class needed for a function
- Signature and docstring are the contract
- Docstring is prompt text, not a comment
- Return a dict, not a bare value
- Defaults never reach the model
basics
~20 sGoogle ADK wraps any callable placed in an agent's tools list into a FunctionTool. The function name, its type-hinted parameters and its docstring become the declaration the model sees, so the docstring is functional code rather than a comment.
solid answer
~40 sIn ADK you normally just write a typed function and put it in `tools=[...]`; the framework wraps it in `FunctionTool` for you (`FunctionTool(func=my_func)` is the explicit form). ADK derives the declaration sent to the model from the signature: the function name is the tool name, each type-hinted parameter becomes a typed JSON-schema property, and the docstring becomes the description the model reads when deciding whether and how to call it. Two conventions matter. First, return a `dict` — a non-dict return is wrapped as `{"result": value}`, so you lose the chance to label fields; the common shape is `{"status": "success", ...}` or `{"status": "error", "error_message": ...}` so the model can recover from failure. Second, don't rely on default parameter values — ADK's automatic declaration doesn't express them, so make parameters explicit and document them.
code
python · 23 linesfrom google.adk.agents import Agent
def get_weather(city: str) -> dict:
"""Returns the current weather report for a city.
Args:
city: The city name, for example "Tokyo".
Returns:
dict: status "success" with a report, or "error" with error_message.
"""
if city.strip().lower() != "tokyo":
return {"status": "error", "error_message": f"No data for {city}."}
return {"status": "success", "report": "Tokyo: 21C, clear skies."}
root_agent = Agent(
name="weather_agent",
model="gemini-2.0-flash",
instruction="Answer weather questions by calling get_weather.",
tools=[get_weather],
)go deeper
Be able to show a typed function with a real docstring dropped straight into an agent's tools list, and say that the name, parameters and docstring are what the model reads.
Explain the mechanics: signature to JSON schema, docstring to description, dict return convention with a non-dict wrapped as a result field, and why defaults are not expressible.
Talk about failure design and payload size — structured error results over exceptions, trimming returns inside the tool, and async tools for I/O so an invocation never blocks.
Own the conventions across a team: a house shape for tool results, docstrings reviewed as prompt text, and limits on how many tools any one agent carries before you split it.
## The wrapping step ADK's tool surface starts from ordinary Python. When you construct an agent — `Agent(name=..., model=..., instruction=..., tools=[get_weather])` — you do not have to build a tool object first. Any plain callable in the `tools` list is wrapped automatically in `FunctionTool`. The explicit form, `FunctionTool(func=get_weather)`, exists and is useful when you want to construct the wrapper yourself or hold a reference to it, but for everyday work the bare function is the idiom. Both `def` and `async def` functions are supported. An async tool is awaited by the framework, which is the right choice for anything doing network or database I/O, because a blocking call inside a tool stalls the invocation that is running it. ## What the model actually sees The model never sees your Python. It sees a *function declaration* that ADK generates from the signature: - **name** — the Python function name. Keep it verb-shaped and specific (`get_invoice_status`, not `handler`). - **parameters** — each type-hinted parameter becomes a typed property in a JSON schema. Stick to simple, JSON-friendly types: `str`, `int`, `float`, `bool`, `list`, `dict`. Exotic types either fail to map cleanly or produce a schema the model fills in badly. - **description** — the docstring, verbatim. This is the entire prompt-side documentation of your tool. That last point is the one candidates underrate. In ADK a docstring is not a comment for humans; it is production prompt text shipped to the model on every turn. A vague docstring shows up in production as the wrong tool being called, or the right tool being called with garbage arguments. Write what the tool does, *when the model should use it*, what each argument means with an example value, and what the return value looks like. ## Default values ADK's automatic declaration does not carry Python default values — the model has no notion of "omit this and let the runtime fill it in." The practical rule is to treat every parameter as something the model must supply, and to document in the docstring what to pass when there is nothing meaningful (for instance, an explicit empty string or a documented sentinel), rather than leaning on a default that the model can never see. ## Return values Return a dictionary. If you return something else — a string, a list, a number — ADK wraps it as `{"result": value}` before handing it back to the model, so it arrives as an unlabelled blob. A dict lets you name the fields the model should reason over, and lets you signal failure in-band: ``` {"status": "error", "error_message": "No account matches that id."} ``` This is why raising an exception is usually the wrong failure mode inside a tool: an exception aborts the step, while a structured error result lets the model apologise, ask a clarifying question, or try a different tool. Reserve exceptions for genuine programmer errors. Keep results small. Whatever the tool returns is serialized into the conversation and re-sent on subsequent turns; a tool that returns a 5 MB JSON document poisons the rest of the session with tokens the model does not need. ## The hidden parameter One parameter is special. If you declare a parameter annotated with ADK's `ToolContext` type, the framework injects it at call time and strips it from the declaration the model sees — the model never knows it exists and never supplies it. That is how a tool reads and writes session state, sets actions, or reaches artifacts and memory, without the model having to pass anything. ## What happens around the call The sequence per call is: the model emits a function call; ADK's flow resolves the name to your wrapped function; any registered before-tool callback runs and may short-circuit; your function executes; ADK emits a function-response event carrying the returned dict; the model sees that response and either answers or calls another tool. Every one of those steps is an event in the session, which is why ADK's dev UI can show you the exact arguments and results of each call. ## What interviewers listen for They want to hear that the docstring and type hints are the contract, not decoration; that dict returns with an explicit status field are the convention; that you don't hide required inputs behind defaults; and that side effects on the conversation go through the injected context object rather than through globals.
- Why prefer returning an error dict over raising an exception inside an ADK tool?A structured error result travels back to the model as a normal function response, so the model can apologise, ask for a corrected argument, or pick a different tool. An exception aborts the step instead, and the user typically sees a generic failure with no recovery path. Reserve exceptions for genuine bugs, and use `{"status": "error", "error_message": ...}` for anything the model could reasonably act on.
- What changes if you make the tool function async?Nothing changes in the declaration the model sees — the name, parameters and docstring are derived the same way. ADK awaits the coroutine instead of calling it directly, which keeps the event loop free while the tool waits on I/O. For any tool doing HTTP or database work, async is the correct default; a blocking call inside a sync tool stalls the invocation running it.
- How would you keep a tool that returns a large payload from bloating the session?Reduce inside the tool, not after. Return the few fields the model needs, cap list lengths, and hand back an identifier or an artifact reference for the bulk rather than the bulk itself. Everything a tool returns becomes an event in the session and is re-sent on later turns, so a fat result costs tokens and latency on every subsequent step, not just once.
The function body is what runs; the signature plus docstring is the job advert the model reads. If the advert is vague, the wrong candidate applies with the wrong paperwork.
saying these in an interview costs you the question
- Thinks you must subclass a Tool class for every function
- Treats the docstring as an optional comment
- Returns bare strings and expects the model to parse them
- Relies on Python default values the model never sees
- Raises exceptions for expected failures like a missing record