Mastering Python’s typing Module: From Basics to AI Agent Tooling
🔹 Introduction
Mastering Python’s typing Module: From Basics to AI Agent Tooling

🔹 Introduction
Welcome to the first article in my series on “Modern Python Libraries for AI Agent Frameworks”. As modern Python developers, especially those building with agent frameworks like Google ADK, CrewAI, or LangGraph, clarity and structure in your code are no longer optional — they’re essential.
The typing module helps us write smarter, safer, and more collaborative code by allowing us to annotate the types of variables, function arguments, and return values.
This article walks you from the fundamentals of typing to advanced patterns, including how typing powers real-world AI agent systems.
📘 What Is the typing Module?
The typing module gives you a way to hint what types your code expects — but it does not enforce those types at runtime. Instead, it:
- Improves editor autocompletion and code readability
- Helps catch bugs early with tools like mypy
- Enables frameworks (like ADK) to extract tool and message metadata
📦 Basic Type Hints
name: str = "Ayman"
age: int = 30
is_ready: bool = True
📌 Explanation:
We’re assigning types to variables. This helps the editor and static tools understand what kind of values are expected.
🧩 Function Annotations
def greet(name: str) -> str:
return f"Hello, {name}"
📌 Explanation:
This function takes a string and returns a string. Type hints make this clear and enforceable through static analysis.
🧺 List, Dict, Tuple, and Set
These are generic container types provided by the typing module. You can use them to declare the types of elements inside collections like lists, dictionaries, tuples, and sets.
from typing import List, Dict, Tuple, Set
# A list of strings
names: List[str] = ["Ali", "Sara"]
# A dictionary with string keys and integer values
ages: Dict[str, int] = {"Ali": 30, "Sara": 25}
# A tuple with two floats
point: Tuple[float, float] = (1.2, 3.4)
# A set of strings
tags: Set[str] = {"ai", "python"}
📌 Explanation:
- List[str]: a list where each item is a string
- Dict[str, int]: a dictionary with string keys and integer values
- Tuple[float, float]: a fixed-size tuple of two floats
- Set[str]: a set where all elements are strings
These hints allow editors and tools to give better autocompletion, refactoring, and bug detection.
💡 Why Use These?
- Adds structure and readability to your data models
- Prevents inserting wrong types into collections
- Helps tools like mypy catch bugs early
- Critical in data-heavy apps, APIs, and agent configurations
🔀 Optional and Union
- Optional[X] is shorthand for Union[X, None], meaning a value can be of type X or None.
- Union[X, Y] means a value can be either type X or type Y.
These are useful when a function or variable may accept more than one type.
from typing import Optional, Union
# Example 1: Optional value
nickname: Optional[str] = None
# Example 2: Union type
value: Union[int, float] = 42.0
# Function that accepts Union type
def show_value(v: Union[int, float]) -> str:
return f"Value is: {v}"
# Function that accepts Optional string
def greet(nick: Optional[str]) -> str:
if nick:
return f"Hi, {nick}!"
return "Hi there!"
# Usage
print(greet("Sara")) # ✅ "Hi, Sara!"
print(greet(None)) # ✅ "Hi there!"
print(show_value(10)) # ✅ "Value is: 10"
print(show_value(3.14)) # ✅ "Value is: 3.14"
📌 Explanation:
- Optional[str] allows either a string or None.
- Union[int, float] means the variable can be either an int or a float.
💡 Why Use Optional and Union?
- Makes functions and variables more flexible
- Improves documentation and tooling support
- Helps catch incompatible types during development
- Encourages thoughtful handling of None and mixed-type inputs
- Optional[str] is the same as Union[str, None]
- Union allows multiple possible types
🎯 Literal
Literal is used to restrict a variable to a fixed set of constant values. This is useful for type safety when only a limited set of values is allowed — like choices, commands, or directions.
It’s commonly used in agents, configs, and LLM tool parameters to ensure predictable inputs.
from typing import Literal
direction: Literal["up", "down", "left", "right"] = "up"
def move(direction: Literal["up", "down", "left", "right"]) -> str:
return f"Moving {direction}"
# Usage
print(move("left")) # ✅
# print(move("back")) # ❌ mypy will flag this
📌 Explanation:
- Literal[…] allows only the specified values.
- Useful for static checking, editor suggestions, and API schema generation.
- Ensures that invalid inputs (like “back”) are caught before runtime.
💡 Why Use Literal?
- Guarantees strict value sets
- Helpful in enums, config options, or controlled command inputs
- Boosts type safety in agent actions or LLM tool arguments
- Improves editor autocomplete and developer experience
📞 Callable
Callable is a type hint used to specify that a parameter or variable should be a function. It lets you define the function’s expected input types and return type.
This is especially useful when you’re writing code that executes other functions dynamically — a common pattern in AI agent tool execution.
from typing import Callabl
def run(tool: Callable[[int, int], int], a: int, b: int) -> int:
return tool(a, b)
def add(x: int, y: int) -> int:
return x + y
def multiply(x: int, y: int) -> int:
return x * y
print(run(add, 2, 3)) # Output: 5
print(run(multiply, 4, 5)) # Output: 20
📌 Explanation:
- run() is a generic function dispatcher.
- tool must be a function that takes two ints and returns an int.
- You can pass any compatible function (add, multiply, etc.) to run().
💡 Why Use a run() Wrapper?
- Centralized Control
- All function calls are routed through run(), so you can:
- Add logging
- Handle errors
- Apply decorators globally
- Add pre/post-processing logic
- Apply Decorators Once
You can decorate run() instead of every individual tool.
def logger(func):
def wrapper(tool, a, b):
print(f"🔧 Calling {tool.__name__} with {a} and {b}")
result = func(tool, a, b)
print(f"✅ Result: {result}")
return result
return wrapper
@logger
def run(tool: Callable[[int, int], int], a: int, b: int) -> int:
return tool(a, b)
print(run(add, 2, 3))
3. Strategy Pattern in Practice
Each function (e.g., add, multiply) is a “strategy.”
run() becomes your strategy executor.
This pattern is widely used in:
- Agent frameworks
- Task runners
- LLM tool execution engines
🧱 TypedDict
TypedDict allows you to define the expected structure of a dictionary with named keys and specific value types. It gives you the flexibility of dicts with the safety of a structured model — perfect for working with JSON-style data.
from typing import TypedDict
class ToolInput(TypedDict):
query: str
max_results: int
def search(input: ToolInput) -> str:
return f"Searching for: {input['query']} (max {input['max_results']})"
# Calling the function
result = search({
"query": "python typing module",
"max_results": 5
})
print(result) # Output: Searching for: python typing module (max 5)
📌 Explanation:
- ToolInput describes a dictionary with two specific keys and their types.
- You call search() by passing a dictionary that matches this structure.
- Python won’t enforce the types at runtime, but tools like mypy will check them.
💡 Why Use TypedDict?
- Provides lightweight schema for dicts
- Compatible with APIs, JSON, or tool inputs
- Easy to combine with static checkers
- Great for agent tool input/output modeling
🧬 Protocol
A Protocol defines a structural interface — any class that implements the expected method(s) is considered valid, even if it doesn’t inherit from the protocol. It’s like an interface in other languages but works with Python’s duck typing.
from typing import Protocol
# Step 1: Define the protocol (interface)
class Executable(Protocol):
def execute(self, task: str) -> str: ...
# Step 2: Implement concrete classes
class MathAgent:
def execute(self, task: str) -> str:
return f"Solving math: {task}"
class SearchAgent:
def execute(self, task: str) -> str:
return f"Searching for: {task}"
# Step 3: Use them interchangeably
def run_agent(agent: Executable, task: str) -> None:
print(agent.execute(task))
# Step 4: Test it
math_agent = MathAgent()
search_agent = SearchAgent()
run_agent(math_agent, "2 + 2")
run_agent(search_agent, "latest AI news")
📌 Explanation:
- Executable defines the required interface.
- MathAgent and SearchAgent both match it, without explicitly inheriting.
- run_agent() can accept any Executable-like object.
💡 Why Use Protocol?
- Acts like an interface without enforcing inheritance
- Encourages clean, flexible architecture (especially in agent systems)
- Ideal for LLM agents, plugins, or tools with common behavior
- Helps catch missing methods at development time using tools like mypy
🧾 NewType
NewType allows you to create a new, distinct type from an existing one (like int, str, etc.) for semantic clarity. It helps you prevent logical errors by distinguishing types that are structurally the same but conceptually different.
from typing import NewType
# Step 1: Define semantic types
UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)
# Step 2: Functions that use these types
def show_user(user_id: UserId) -> str:
return f"Showing user with ID {user_id}"
def show_product(product_id: ProductId) -> str:
return f"Showing product with ID {product_id}"
# Step 3: Creating typed instances
uid = UserId(1001)
pid = ProductId(42)
# Step 4: Calling the functions
print(show_user(uid)) # ✅ OK
print(show_product(pid)) # ✅ OK
# print(show_user(pid)) # ❌ Type checker like mypy will catch this
📌 Explanation:
- UserId and ProductId are both technically int at runtime.
- But they are treated as different types by static checkers.
- This avoids accidentally mixing up values like user IDs and product IDs.
💡 Why Use NewType?
- Distinguishes concepts that share structure but differ in meaning
- Makes APIs safer and more expressive
- Helps catch bugs during static type checking
- No runtime cost — it’s still an int under the hood
🧭 Modern Python Syntax (3.9+)
names: list[str] = ["Ayman", "Sara"]
scores: dict[int, str] = {1: "one"}
📌 Explanation:
You no longer need to import List, Dict, etc. from typing. This cleaner syntax is available from Python 3.9+.
💡 Tip: Use the modern style if you’re on Python 3.9 or newer.
❗ Typing Is Not Runtime Enforcement
from typing import Dict
person_ages: Dict[str, int] = {
"Alice": "yy", # Wrong type, but Python doesn't raise an error
"Bob": 12
}
print(person_ages) # Works fine at runtime!
📌 Explanation:
Typing helps humans and static checkers — but Python won’t stop incorrect types at runtime.
📊 When to Use Typing — and When Not To
✅ Use Typing When:
- You build agent tools or pass functions, ensuring clear interfaces for dynamic systems like Google ADK or LangGraph.
- You want editor support and static checks with tools like
mypyfor better autocompletion and error detection. - You write libraries, APIs, or agent systems where type safety and documentation are critical.
❌ Don’t Rely on Typing Alone When:
- You need runtime type enforcement (pair with
pydanticorFastAPIfor validation, as we’ll explore in this series). - You’re writing quick throwaway scripts where speed trumps long-term maintainability.
- Your project skips static analysis (e.g., no
mypyor IDE type checking), limiting the benefits of type hints.
🔧 Putting It All Together
Below is a full example — no external libraries — showing how each feature composes into a simple agent tool system. Copy, paste, and run!
# 🚀 Real Simple Agent Tool System
from typing import (
TypedDict, Protocol, Callable, Optional,
Literal, NewType, Dict
)
# 1️⃣ NewType for semantic IDs
AgentId = NewType("AgentId", int)
TaskId = NewType("TaskId", str)
# 2️⃣ TypedDict for structured inputs
class TaskInput(TypedDict):
query: str
top_k: int
verbose: Optional[bool]
# 3️⃣ Literal for restricted task names
TaskType = Literal["search", "summarize", "translate"]
# 4️⃣ Protocol for any tool that implements execute()
class AgentTool(Protocol):
def execute(self, task: TaskType, data: TaskInput) -> str: ...
# Registry mapping TaskType to tool functions
ToolRegistry: Dict[TaskType, Callable[[TaskInput], str]] = {}
# 5️⃣ Decorator to register tools
def register_tool(task_type: TaskType):
def decorator(func: Callable[[TaskInput], str]):
ToolRegistry[task_type] = func
return func
return decorator
# Tool implementations
@register_tool("search")
def search_tool(data: TaskInput) -> str:
verbose_msg = " (verbose mode)" if data.get('verbose') else ""
return f"🔍 Searching '{data['query']}' with top_k={data['top_k']}{verbose_msg}"
@register_tool("summarize")
def summarize_tool(data: TaskInput) -> str:
return f"📝 Summarizing query: {data['query']} (top {data['top_k']} sources)"
@register_tool("translate")
def translate_tool(data: TaskInput) -> str:
return f"🌐 Translating '{data['query']}' (top {data['top_k']} languages)"
# Dispatcher function
def dispatch(task_type: TaskType, data: TaskInput) -> str:
tool = ToolRegistry.get(task_type)
if tool:
return tool(data)
return f"❌ Task '{task_type}' not found in registry"
# 🎯 Usage demo
if __name__ == "__main__":
task_data: TaskInput = {
"query": "Python agent frameworks",
"top_k": 5,
"verbose": True
}
print("🤖 Agent System Demo:")
print("-" * 40)
print(dispatch("search", task_data))
print(dispatch("summarize", task_data))
print(dispatch("translate", task_data))
print(dispatch("invalid", task_data)) # type: ignore
print("-" * 40)
print("✅ All typing concepts demonstrated!")
🧠 Final Thoughts
Python’s typing module is a cornerstone of modern development — especially for AI agent frameworks like Google ADK, CrewAI, and LangGraph. Why use it?
- Boosts editor autocomplete and refactoring for faster, error-free coding.
- Catches type mismatches early with mypy.
- Documents your intent, making code clearer for collaborators and your future self.
When to skip typing: only for quick prototypes or scripts where rapid iteration matters more than long-term safety and maintainability.
For production-grade systems, pair type hints with static analysis via mypy and add runtime enforcement with libraries like pydantic or FastAPI. We’ll cover those next in this series on modern Python libraries for agent frameworks. Try running mypy on the example above to see type checking in action, and let me know in the comments which agent frameworks you use most — your feedback will drive our future deep dives!
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 200k supporters? We do not get paid by Medium!
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok and Instagram. And before you go, don’t forget to clap and follow the writer️!
메타데이터
- post_id
- 0e2e004fed5b
- slug
- mastering-pythons-typing-module-from-basics-to-building-agents-0e2e004fed5b
- url
- https://python.plainenglish.io/mastering-pythons-typing-module-from-basics-to-building-agents-0e2e004fed5b
- canonical_url
- https://python.plainenglish.io/mastering-pythons-typing-module-from-basics-to-building-agents-0e2e004fed5b
- author_url
- https://medium.com/@ayman3000
- status
- ok
- fetched_at
- 2026-07-13 12:56:55