Type Hints
Python does not require you to declare types. Type hints let you write them down anyway, as notes for your editor, your tools and the next person to read the file.
In agent code they are not optional in practice, because frameworks read them. The type hints on a tool function are what become the schema the model sees. Leave them off and the tool does not work properly.
The basic form
def add(a: int, b: int) -> int:
return a + ba: int— this parameter should be a whole number-> int— this function gives back a whole number
Variables can be annotated too, though you need it far less often:
name: str = "Priya"
retries: int = 0Python does not enforce them
This is the part that surprises people from Java and C#.
def add(a: int, b: int) -> int:
return a + b
print(add("hello", "world"))That prints helloworld. No error. The hint is a note, not a rule — the interpreter ignores it entirely.
So what is the point?
- Your editor uses them for autocomplete and warns you before you run
- A type checker such as
mypyor Pyright catches mistakes across the whole project - Frameworks read them at runtime to build schemas
- A reader learns what a function expects without running it
The first and last are worth it on their own. The third is why this page sits in this tutorial.
The types you will actually use
from typing import Optional, Any
def f(
text: str,
count: int,
ratio: float,
enabled: bool,
items: list[str],
config: dict[str, int],
pair: tuple[int, str],
maybe: Optional[str] = None,
anything: Any = None,
) -> None:
...| Hint | Meaning |
|---|---|
str, int, float, bool | the basic values |
list[str] | a list of strings |
dict[str, int] | keys are strings, values are whole numbers |
tuple[int, str] | exactly two items, in that order |
Optional[str] | a string, or None |
Any | anything at all; switches checking off |
-> None | returns nothing useful |
list[str] with lowercase list works on Python 3.9 and later. Older code imports List from typing and writes List[str]. Both mean the same; prefer the lowercase form.
Optional means "or None"
def find_owner(ticket_id: int) -> Optional[str]:
...This says: you get back a string, or you get None. The caller has been told to check.
Optional[str] is the same as str | None, and the newer | form reads better:
def find_owner(ticket_id: int) -> str | None:
...Note carefully: on a parameter, Optional does not make the argument optional. It makes the type allow None. What makes it optional is a default value.
def f(a: Optional[str]): # required, may be None
def f(a: Optional[str] = None): # optionalWhy frameworks depend on them
Here is the payoff.
from langchain_core.tools import tool
@tool
def get_weather(city: str, unit: str = "celsius") -> str:
"""Get the current weather for a city."""
return f"28 degrees {unit} in {city}"The @tool decorator inspects this function and builds a JSON schema from it:
- the parameter names become the schema's properties
- the type hints become the property types
- the absence of a default makes
cityrequired - the docstring becomes the description the model reads to decide when to call it
Remove the type hints and the framework cannot say what city is. Remove the docstring and the model does not know what the tool is for. Both failures are quiet — the code runs, the agent simply behaves badly.
So in agent code the rule is firm: every tool function gets full type hints and a real docstring.
Callables
When a function takes another function, you can describe its shape:
from typing import Callable
def apply(func: Callable[[int], str], value: int) -> str:
return func(value)Callable[[int], str] means "takes one int, gives back a str". You will see this in framework signatures for nodes, routers and callbacks. Reading it is enough; you rarely write it.
Type aliases
When a shape repeats, name it.
Message = dict[str, str]
def send(history: list[Message]) -> Message:
...Easier to read than list[dict[str, str]] in five places, and you can change it in one spot.
Checking your types
Hints do nothing until something checks them. In an editor, install Pylance or Pyright and you get warnings as you type. On the command line:
pip install mypy
mypy myagent/You do not need this on day one. It becomes worthwhile the moment more than one person touches the code.
How much to annotate
A reasonable standard, and the one this tutorial follows:
- always on tool functions — frameworks depend on it
- always on anything crossing a boundary — public functions, module entry points
- usually on function parameters and return values
- rarely on local variables, where it is obvious and just noise
count = 0
names: list[str] = []The first needs no hint. The second is worth one, because an empty list gives your editor nothing to work from.
Coming from Java or C#
The syntax will feel backwards, and the guarantees are much weaker.
| Java / C# | Python | ||
|---|---|---|---|
| Where the type goes | before the name, int x | after the name, x: int | |
| Return type | before the method name | after the parameters, -> int | |
| Enforced? | yes, by the compiler | no, by nothing at runtime | |
| When mistakes surface | at compile time | when your editor or mypy looks | |
| Optional | Optional<T>, nullable T? | Optional[T], `T \ | None` |
| Generics | List<String> | list[str] |
The hard adjustment: nothing stops a wrong type at runtime. A hint is a claim, not a guarantee. If you need a real guarantee — and for anything a model produced, you do — use a Pydantic model, which actually validates.
That is the division worth remembering: type hints for your tools and your editor, Pydantic for data you cannot trust.
Common mistakes
- Leaving hints off a tool function, so the schema is incomplete and the model misuses it.
- Assuming they are enforced. They are not.
Optional[str]with no= None, expecting the argument to be optional.Anyeverywhere, which switches off the benefit you added them for.- Annotating every local variable, which adds noise without adding information.
Practise this
- Add hints to a function you have already written and see what your editor starts telling you.
- Write a function returning
str | Noneand handle both outcomes at the call site. - Write a tool-shaped function with full hints and a proper docstring, then say out loud what a framework could build from it.
- Deliberately pass the wrong type and confirm Python runs it anyway.