Skip to content
Learn Agentic AI
Learn Agentic AI11 min read16 views

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.

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →
flowchart LR
    INPUT(["User intent"])
    PARSE["Parse plus<br/>classify"]
    PLAN["Plan and tool<br/>selection"]
    AGENT["Agent loop<br/>LLM plus tools"]
    GUARD{"Guardrails<br/>and policy"}
    EXEC["Execute and<br/>verify result"]
    OBS[("Trace and metrics")]
    OUT(["Outcome plus<br/>next action"])
    INPUT --> PARSE --> PLAN --> AGENT --> GUARD
    GUARD -->|Pass| EXEC --> OUT
    GUARD -->|Fail| AGENT
    AGENT --> OBS
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
    style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style OUT fill:#059669,stroke:#047857,color:#fff
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.

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.

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

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

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.