Tool.func typing

Following up this thread where I got some answers about unit testing tools, I’ve noticed that when using the `.func()` method on a tool, I couldn’t get any typing info.

# Tool definition
@tool
def echo(foo:str)->str:
   return foo

# In my unit test
echo.func("bar") #  works but not typed, no type completion

Note that for simplicity’s sake I don’t want to export an undecorated function, as I think it defeats the purpose of having a decorator in the first place.
The problem seems to be that “echo” is of type `BaseTool`which doesn’t have the .func method, contrary to Tool.

Anything I can do to improve this situation? Static typing is really useful to have robust unit tests.

hi @eric-burel

you can check what’s the runtime type of the tool. It should show something like this:

from langchain_core.tools import tool
from langchain_core.tools.structured import StructuredTool
from langchain_core.tools.simple import Tool

@tool
def echo(foo: str) -> str:
    """Echo."""
    return foo

type(echo).__name__ # 'StructuredTool'
isinstance(echo, StructuredTool) # True

@tool(infer_schema=False)
def echo2(foo: str) -> str:
    """Echo."""
    return foo

type(echo2).__name__ # 'Tool'
isinstance(echo2, Tool) # True

Both StructuredTool and Tool define .func - but their common parent BaseTool does not.

Some workarounds:

1. StructuredTool.from_function()

from langchain_core.tools import StructuredTool

def echo(foo: str) -> str:
    """Echo the input."""
    return foo

echo_tool = StructuredTool.from_function(echo)

# Type checker knows echo_tool is StructuredTool → .func is valid
echo_tool.func("bar")  # typed! autocomplete works

For testing, you get the best of both worlds:

# Unit test - call .func with full type safety
def test_echo_logic():
    assert echo_tool.func("hello") == "hello"

# Integration test - exercise validation, callbacks, the full pipeline
def test_echo_pipeline():
    assert echo_tool.invoke({"foo": "hello"}) == "hello"

2. isinstance type narrowing in tests

from langchain_core.tools import tool, StructuredTool

@tool
def echo(foo: str) -> str:
    """Echo the input."""
    return foo

# In your test:
def test_echo():
    assert isinstance(echo, StructuredTool)  # narrows type for the checker
    # After isinstance, type checker knows echo is StructuredTool
    result = echo.func("bar")  # now typed! no error
    assert result == "bar"

The isinstance check is also a useful runtime guard: if LangChain ever changes what @tool returns, your test fails immediately with a clear message instead of a cryptic AttributeError.

3. cast

from typing import cast
from langchain_core.tools import tool, StructuredTool

@tool
def echo(foo: str) -> str:
    """Echo the input."""
    return foo

echo_typed = cast(StructuredTool, echo)
echo_typed.func("bar")  # typed

This is a type-only assertion, no runtime cost - but it doesn’t verify anything at runtime like isinstance does.

does it work for you @eric-burel ?

Hi Pawel, honestly no, this doesn’t solve the issue. At best you get the `.func` method to be recognized as existing since StructuredTools exists, but you don’t have any sort of typing, it’s just “any→None” basically. This is a very literal answer but doesn’t address the question of getting proper typing in unit test within getting rid of the decorator (which would make the decorator quite useless).