Post

Dict, TypedDict, Annotated, and Field, BaseModel

Let’s Understanding Python typing tools and Pydantic models.

Dict, TypedDict, Annotated, and Field, BaseModel

When building LLM-based workflows using LangGraph and LangChain, one of the most fundamental concepts is managing state. This “state” refers to the data passed between nodes, retained memory, routing decisions, and more.

To define and validate state structures effectively, we often rely on a few core Python typing tools and Pydantic models. In this post, we’ll break down the following key constructs that frequently appear in LangGraph workflows:

  • Dict
  • TypedDict
  • Annotated
  • Field
  • BaseModel

Based on research, in LangGraph, defining state using a combination of TypedDict and Annotated is generally considered the safest and most intuitive approach.
While using BaseModel is not necessarily wrong, it introduces a dependency on pydantic, which can be an additional burden.
As a result, frequent conversions using .dict() may be required, adding extra overhead.
Ultimately, since LangGraph emphasizes lightweight internal design, TypedDict is considered more suitable.

Dict

In the previous post, we learned about Dict, Any, Optional from the typing module to clarify type definitions. In Python, Dict is used only for static type checking and does not enforce type restrictions at runtime.
This is an example that shows how the static type checker mypy responds when Python type hints (such as Dict[str, str]) do not match the actual values.
As mentioned earlier, errors like these are not detected at runtime.

  1. Case 1. Type Match (Valid Behavior)
1
2
3
4
5
from typing import Dict

state1: Dict[str, str] = {"tool_used": "search", "count": "1", "result": "OK"}
print(type(state1))
print(state1)
  • The type hint Dict[str, str] specifies that all keys and values must be strings.
  • Since all the actual values are strings, mypy performs type checking without raising any errors.
1
2
3
# Run in Jupyter Notebook
<class 'dict'>
{'tool_used': 'search', 'count': '1', 'result': 'OK'}
1
2
3
# mypy zMyImsi.py
(MyDev) D:\02.MyCode\GP-MyReference\14.MyLLM_LangGraph>mypy zMyImsi.py
Success: no issues found in 1 source file
  1. Case 2. Type Mismatch (count is int)
1
2
3
4
5
from typing import Dict

state2: Dict[str, str] = {"tool_used": "search", "count": 1, "result": "OK"}
print(type(state2))
print(state2)
  • The value 1 assigned to "count" is an integer, which violates the Dict[str, str] type hint.
  • when using mypy for type checking, it detects a type mismatch error.
  • However, since Python is a dynamically typed language, it runs without any error at runtime.
1
2
3
# Run in Jupyter Notebook
<class 'dict'>
{'tool_used': 'search', 'count': 1, 'result': 'OK'}
1
2
3
4
# mypy zMyImsi.py
(MyDev) D:\02.MyCode\GP-MyReference\14.MyLLM_LangGraph>mypy zMyImsi.py
zMyImsi.py:9: error: Dict entry 1 has incompatible type "str": "int"; expected "str": "str"  [dict-item]
Found 1 error in 1 file (checked 1 source file)

TypedDict

Since writing this post, I’ve decided to use TypedDict instead of dict when working with LangGraph.
Just like with Dict, Type errors are not caught at runtime, They can be detected by static type checkers like mypy.
The key difference is that TypedDict can detect missing required keys based on the defined structure.

  1. Case 1. Type Match (Valid Behavior)
1
2
3
4
5
6
7
8
9
10
from typing import Dict, TypedDict

class MyState(TypedDict):
    tool_used: str
    count : int
    result: str

state1: MyState = {"tool_used": "search", "count": 1, "result": "OK"}
print(type(state1))
print(state1)
  • MyState is a TypedDict with three explicitly defined fields.
  • Since all field types match exactly, the mypy static type check passes without any issues.
1
2
3
# Run in Jupyter Notebook
<class 'dict'>
{'tool_used': 'search', 'count': 1, 'result': 'OK'}
1
2
3
# mypy zMyImsi.py
(MyDev) D:\02.MyCode\GP-MyReference\14.MyLLM_LangGraph>mypy zMyImsi.py
Success: no issues found in 1 source file
  1. Case 2. Type Mismatch (count is str)
1
2
3
4
5
6
7
8
9
10
from typing import Dict, TypedDict

class MyState(TypedDict):
    tool_used: str
    count : int
    result: str

state1: MyState = {"tool_used": "search", "count": "1", "result": "OK"}
print(type(state1))
print(state1)
  • "count" is defined as an int, but "1" is a str, resulting in a type mismatch.
  • when using mypy for type checking, it detects a type mismatch error.
  • However, since Python is a dynamically typed language, it runs without any error at runtime.
1
2
3
# Run in Jupyter Notebook
<class 'dict'>
{'tool_used': 'search', 'count': '1', 'result': 'OK'}
1
2
3
4
# mypy zMyImsi.py
(MyDev) D:\02.MyCode\GP-MyReference\14.MyLLM_LangGraph>mypy zMyImsi.py
zMyImsi.py:13: error: Incompatible types (expression has type "str", TypedDict item "count" has type "int")  [typeddict-item]
Found 1 error in 1 file (checked 1 source file)
  1. Case 3. Missing Key (count is absent)
1
2
3
4
5
6
7
8
9
10
from typing import Dict, TypedDict

class MyState(TypedDict):
    tool_used: str
    count : int
    result: str

state3: MyState = {"tool_used": "search", "result": "OK"}
print(type(state3))
print(state3)
  • By default, TypedDict considers all fields as required.
  • Since the "count" key is missing, mypy detects it as an error.
  • However, since Python is a dynamically typed language, it runs without any error at runtime.
1
2
3
# Run in Jupyter Notebook
<class 'dict'>
{'tool_used': 'search', 'result': 'OK'}
1
2
3
4
# mypy zMyImsi.py
(MyDev) D:\02.MyCode\GP-MyReference\14.MyLLM_LangGraph>mypy zMyImsi.py
zMyImsi.py:10: error: Missing key "count" for TypedDict "MyState"  [typeddict-item]
Found 1 error in 1 file (checked 1 source file)

Annotated

Annotated is a feature provided by Python’s typing module that allows you to attach metadata (additional information) to existing types.

  • It enhances code readability and documentation quality by combining type hints with descriptive annotations.
  • It is commonly used in frameworks like LangGraph when defining state, to specify field-level descriptions, default values, or fixed values as additional metadata.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import TypedDict
from typing import Annotated
from pydantic import Field

# TypedDict 사용

class MyState(BaseModel):
    tool_used: Annotated[str, Field(..., min_length=3, max_length=50, description="Name of the tool being called")]
    count: Annotated[int, Field(gt=1, lt=10, description="Number of times the node has executed (1–10)")]
    edge: Annotated[int, Field(..., description="Number of edges connected to the node (1–10)")]
    result: Annotated[str, Field(..., description="Execution result of the node")]
    
state1: MyState = {"tool_used": "search", "count": 1, "edge": 3, "result": "OK"}
print(type(state1))
print(state1)
1
2
<class 'dict'>
{'tool_used': 'search', 'result': 'OK'}

BaseModel

Using pydantic.BaseModel, you can convert dictionary-based data into a class-based model that supports runtime validation, automatic type conversion, and serialization.
However, as mentioned earlier, since LangGraph emphasizes lightweight internal design, TypedDict is considered more suitable than BaseModel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from typing import Annotated
from pydantic import BaseModel, Field, ValidationError

# Define the state using BaseModel
class MyState(BaseModel):
    tool_used: Annotated[str, Field(..., min_length=3, max_length=50, description="Name of the tool being called")]
    count: Annotated[int, Field(gt=1, lt=10, description="Number of times the node has executed (1–10)")]
    edge: Annotated[int, Field(..., description="Number of edges connected to the node (1–10)")]
    result: Annotated[str, Field(..., description="Execution result of the node")]

# Attempt to create a valid instance
try:
    state1 = MyState(tool_used="search", count=5, edge=3, result="OK")
    print("Valid state data:", state1)
except ValidationError as e:
    print("Validation error:", e)

# Attempt to create an invalid instance
try:
    state2 = MyState(tool_used="s", count=111, edge=3, result="OK")
    print("Valid state data:", state2)
except ValidationError as e:
    print("Validation error:")
    for error in e.errors():
        print(f"- {error['loc'][0]}: {error['msg']}")
1
2
3
4
5
# Run in Jupyter Notebook
Valid state data: tool_used='search' count=5 edge=3 result='OK'
Validation error:
- tool_used: String should have at least 3 characters
- count: Input should be less than 10
This post is licensed under CC BY 4.0 by the author.