Python Type Hints for AI Code: Writing Self-Documenting Agent Applications
Master Python type hints for AI engineering including generics, TypedDict, Protocol classes, and runtime validation to build maintainable agent applications with self-documenting interfaces.
Why Type Hints Matter in AI Codebases
AI agent code is notoriously difficult to maintain. Functions accept dictionaries with vague structures, tool outputs come back as Any, and model responses get passed around as untyped strings. When a codebase grows past a few hundred lines, developers spend more time reading code than writing it. Type hints solve this by making data shapes explicit at every boundary.
Python type hints do not add runtime overhead. They are metadata that type checkers like mypy and pyright use to catch bugs before you deploy. For AI engineers, they also serve as living documentation that stays synchronized with the code.
Essential Types for Agent State
The typing module provides the building blocks. Start with the basics and layer complexity only when needed.
from typing import Optional, Union, Literal
# Agent configuration with clear types
class AgentConfig:
model: str = "gpt-4o"
temperature: float = 0.7
max_tokens: Optional[int] = None
mode: Literal["chat", "function_call", "streaming"] = "chat"
# Tool results can be multiple types
ToolResult = Union[str, dict[str, any], list[dict[str, any]]]
TypedDict for Structured Messages
When your agent passes messages around as dictionaries, TypedDict gives you type safety without changing runtime behavior.
from typing import TypedDict, Required, NotRequired
class ChatMessage(TypedDict):
role: Required[Literal["user", "assistant", "system", "tool"]]
content: Required[str]
name: NotRequired[str]
tool_call_id: NotRequired[str]
class ToolDefinition(TypedDict):
name: str
description: str
parameters: dict[str, any]
def build_prompt(messages: list[ChatMessage]) -> list[ChatMessage]:
system_msg: ChatMessage = {
"role": "system",
"content": "You are a helpful agent.",
}
return [system_msg] + messages
Protocol Classes for Agent Interfaces
Protocol defines structural subtyping. Any class that implements the required methods satisfies the protocol without explicit inheritance. This is perfect for agent tool systems where you want pluggable implementations.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from typing import Protocol, runtime_checkable
@runtime_checkable
class AgentTool(Protocol):
name: str
description: str
async def execute(self, arguments: dict[str, any]) -> str: ...
class WebSearchTool:
name = "web_search"
description = "Search the web for information"
async def execute(self, arguments: dict[str, any]) -> str:
query = arguments["query"]
# perform search
return f"Results for: {query}"
# This works because WebSearchTool matches AgentTool structurally
def register_tool(tool: AgentTool) -> None:
assert isinstance(tool, AgentTool) # runtime_checkable enables this
print(f"Registered tool: {tool.name}")
Generics for Reusable Agent Components
Generics let you write components that work with any type while preserving type information through the call chain.
from typing import TypeVar, Generic
T = TypeVar("T")
class AgentMemory(Generic[T]):
def __init__(self) -> None:
self._store: list[T] = []
def add(self, item: T) -> None:
self._store.append(item)
def get_recent(self, n: int = 5) -> list[T]:
return self._store[-n:]
# Type checker knows this stores ChatMessage objects
memory: AgentMemory[ChatMessage] = AgentMemory()
memory.add({"role": "user", "content": "Hello"})
recent = memory.get_recent(3) # inferred as list[ChatMessage]
Runtime Validation with Type Guards
Type hints alone are compile-time. For AI applications that receive unpredictable data from APIs, combine hints with runtime guards.
from typing import TypeGuard
def is_valid_tool_call(data: dict) -> TypeGuard[ToolDefinition]:
return (
isinstance(data.get("name"), str)
and isinstance(data.get("description"), str)
and isinstance(data.get("parameters"), dict)
)
def process_model_output(raw: dict) -> None:
if is_valid_tool_call(raw):
# type checker now knows raw is ToolDefinition
print(f"Calling tool: {raw['name']}")
else:
print("Invalid tool call structure")
FAQ
When should I use TypedDict versus a Pydantic model?
Use TypedDict when you need typed dictionaries that remain plain dicts at runtime, such as when passing data to APIs that expect dictionary arguments. Use Pydantic when you need validation, serialization, and computed fields. TypedDict is lighter weight; Pydantic is more powerful.
Do type hints slow down Python at runtime?
No. Type hints are stored as metadata and are not evaluated during normal execution. The only exception is when you use runtime_checkable protocols with isinstance checks or when libraries like Pydantic inspect annotations for validation.
How do I type hint a function that returns different types based on input?
Use @overload from the typing module to define multiple signatures for the same function. The type checker uses the overload signatures while the actual implementation handles the logic. This is common in agent frameworks where a function might return a string or a structured object depending on the output format parameter.
#Python #TypeHints #AIEngineering #CodeQuality #AgenticAI #LearnAI
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.