Module 21 - Type Hints
Type hints (also called type annotations) allow you to specify expected types for variables, function parameters, and return values, making code more readable and maintainable.
1. Introduction to Type Hints
Python is dynamically typed, but type hints provide optional static typing for better code documentation and error detection.
# Without type hints
def greet(name):
return f"Hello, {name}!"
# With type hints
def greet(name: str) -> str:
return f"Hello, {name}!"
Type hints are optional and not enforced at runtime. They're checked by static analysis tools like mypy.
2. Basic Type Hints
2.1 Variable Annotations
# Basic types
name: str = "Alice"
age: int = 25
height: float = 5.6
is_student: bool = True
# Without assignment
count: int
count = 10
# Multiple variables
x: int
y: int
x, y = 10, 20
2.2 Function Annotations
def add(x: int, y: int) -> int:
"""Add two integers"""
return x + y
def greet(name: str, excited: bool = False) -> str:
"""Greet someone"""
greeting = f"Hello, {name}!"
return greeting + "!!!" if excited else greeting
# Function with no return (returns None)
def print_message(message: str) -> None:
print(message)
2.3 Collection Types
from typing import List, Dict, Set, Tuple
# Lists
numbers: List[int] = [1, 2, 3, 4, 5]
names: List[str] = ["Alice", "Bob", "Charlie"]
# Dictionaries
scores: Dict[str, int] = {"Alice": 95, "Bob": 87}
config: Dict[str, str] = {"host": "localhost", "port": "5432"}
# Sets
unique_ids: Set[int] = {1, 2, 3}
# Tuples (fixed size)
point: Tuple[int, int] = (10, 20)
rgb: Tuple[int, int, int] = (255, 0, 0)
# Tuple with variable length (all same type)
numbers: Tuple[int, ...] = (1, 2, 3, 4, 5)
3. The typing Module
3.1 Optional Types
from typing import Optional
# Can be str or None
def find_user(user_id: int) -> Optional[str]:
"""Return username or None if not found"""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
# Equivalent to Union[str, None]
name: Optional[str] = None
name = "Alice"
3.2 Union Types
from typing import Union
# Can be int or float
def process_number(value: Union[int, float]) -> float:
return float(value) * 2
# Can be multiple types
identifier: Union[int, str] = 123
identifier = "ABC123"
# Python 3.10+ syntax (using |)
def process(value: int | float) -> float:
return float(value) * 2
3.3 Any Type
from typing import Any
# Accepts any type
def print_value(value: Any) -> None:
print(value)
# Dynamic data
data: Any = {"key": "value"}
data = [1, 2, 3]
data = "string"
3.4 Callable Types
from typing import Callable
# Function that takes int and returns str
def process(func: Callable[[int], str], value: int) -> str:
return func(value)
# Usage
def int_to_str(x: int) -> str:
return str(x)
result = process(int_to_str, 42)
# Multiple parameters
MathFunc = Callable[[int, int], int]
def apply_operation(func: MathFunc, x: int, y: int) -> int:
return func(x, y)
4. Advanced Type Hints
4.1 Generic Types
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self.items: List[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
# Usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
str_stack: Stack[str] = Stack()
str_stack.push("hello")
4.2 Type Aliases
from typing import List, Dict, Tuple
# Create type aliases
Vector = List[float]
Matrix = List[List[float]]
JSONDict = Dict[str, Any]
def scale_vector(vector: Vector, scalar: float) -> Vector:
return [x * scalar for x in vector]
# Complex nested types
UserData = Dict[str, Union[str, int, List[str]]]
user: UserData = {
"name": "Alice",
"age": 25,
"skills": ["Python", "JavaScript"]
}
4.3 Literal Types
from typing import Literal
# Only accept specific values
def set_mode(mode: Literal["read", "write", "append"]) -> None:
print(f"Mode set to: {mode}")
set_mode("read") # ✓ OK
set_mode("delete") # ✗ Type checker error
# With Union
Status = Literal["success", "error", "pending"]
def handle_status(status: Status) -> None:
if status == "success":
print("Operation successful")
4.4 Protocol Types
from typing import Protocol
# Duck typing with protocols
class Drawable(Protocol):
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render(shape: Drawable) -> None:
shape.draw()
# Both work because they implement draw()
render(Circle())
render(Square())
5. Type Checking with mypy
5.1 Installation
pip install mypy
5.2 Basic Usage
example.py:
def add(x: int, y: int) -> int:
return x + y
result = add(5, 10) # ✓ OK
result = add("5", "10") # ✗ Type error
Check with mypy:
mypy example.py
# Output:
# example.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"
# example.py:4: error: Argument 2 to "add" has incompatible type "str"; expected "int"
5.3 Configuration
mypy.ini:
[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
pyproject.toml:
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
6. Practical Examples
6.1 Data Processing
from typing import List, Dict, Tuple
def calculate_statistics(numbers: List[float]) -> Dict[str, float]:
"""Calculate mean, median, and std dev"""
n = len(numbers)
mean = sum(numbers) / n
sorted_nums = sorted(numbers)
median = sorted_nums[n // 2]
variance = sum((x - mean) ** 2 for x in numbers) / n
std_dev = variance ** 0.5
return {
"mean": mean,
"median": median,
"std_dev": std_dev
}
stats = calculate_statistics([1.5, 2.3, 3.7, 4.2])
print(stats)
6.2 API Response Handler
from typing import Dict, List, Optional, Any
User = Dict[str, Any]
def parse_users(api_response: Dict[str, Any]) -> List[User]:
"""Extract users from API response"""
if api_response.get("status") != "success":
return []
users = api_response.get("data", {}).get("users", [])
return users
def find_user_by_id(users: List[User], user_id: int) -> Optional[User]:
"""Find user by ID"""
for user in users:
if user.get("id") == user_id:
return user
return None
# Usage
response: Dict[str, Any] = {
"status": "success",
"data": {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
}
}
users = parse_users(response)
user = find_user_by_id(users, 1)
6.3 Class with Type Hints
from typing import List, Optional
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
grades: List[float]
email: Optional[str] = None
def average_grade(self) -> float:
"""Calculate average grade"""
if not self.grades:
return 0.0
return sum(self.grades) / len(self.grades)
def is_passing(self, threshold: float = 60.0) -> bool:
"""Check if student is passing"""
return self.average_grade() >= threshold
# Usage
student = Student(
name="Alice",
age=20,
grades=[85.5, 90.0, 88.5],
email="alice@example.com"
)
print(f"Average: {student.average_grade():.2f}")
print(f"Passing: {student.is_passing()}")
7. Type Hints Best Practices
7.1 Gradual Typing
# Start with critical functions
def calculate_total(prices: List[float]) -> float:
return sum(prices)
# Add types incrementally
def format_currency(amount): # No types yet
return f"${amount:.2f}"
# Then improve
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
7.2 Using cast()
from typing import cast, Any
def process_data(data: Any) -> str:
# Tell type checker we know this is a string
text = cast(str, data)
return text.upper()
7.3 Type Ignore Comments
# When you need to suppress type checking
result = some_untyped_library_function() # type: ignore
# With explanation
result = legacy_code() # type: ignore[attr-defined]
8. Common Patterns
8.1 Factory Pattern
from typing import Protocol, TypeVar
T = TypeVar('T', bound='Serializable')
class Serializable(Protocol):
def serialize(self) -> str:
...
def create_instance(cls: type[T]) -> T:
"""Factory function"""
return cls()
8.2 Builder Pattern
from typing import TypeVar, Generic
from dataclasses import dataclass
T = TypeVar('T')
class Builder(Generic[T]):
def __init__(self, cls: type[T]) -> None:
self.cls = cls
self.kwargs = {}
def with_field(self, key: str, value: Any) -> 'Builder[T]':
self.kwargs[key] = value
return self
def build(self) -> T:
return self.cls(**self.kwargs)
9. Type Hints vs Runtime Checks
from typing import List
# Type hints (static checking only)
def process_numbers(numbers: List[int]) -> int:
return sum(numbers)
# This passes type checking but fails at runtime
# process_numbers([1, 2, "3"]) # Runtime error
# Add runtime validation
def process_numbers_safe(numbers: List[int]) -> int:
if not all(isinstance(n, int) for n in numbers):
raise TypeError("All items must be integers")
return sum(numbers)
# Or use pydantic for automatic validation
from pydantic import BaseModel
class NumberList(BaseModel):
numbers: List[int]
# This validates at runtime
data = NumberList(numbers=[1, 2, 3])
10. Limitations and Trade-offs
Pros
✅ Better code documentation
✅ Catch errors before runtime
✅ Improved IDE autocomplete
✅ Easier refactoring
✅ Self-documenting code
Cons
❌ More verbose code
❌ Learning curve
❌ Not enforced at runtime
❌ Can be complex for advanced types
❌ Requires external tools (mypy)
Summary
✅ Type hints improve code readability and maintainability
✅ Use typing module for complex types
✅ mypy performs static type checking
✅ Type hints are optional and not enforced at runtime
✅ Start with critical functions, add types gradually
✅ Use type aliases for complex nested types
Next Steps
In Module 22, you'll learn:
- Asynchronous programming with
asyncio asyncandawaitkeywords- Concurrent operations
- Building async applications
Practice Exercises
- Add type hints to an existing Python project
- Create a typed data class for a complex domain model
- Write a type-safe API client with proper annotations
- Configure
mypyfor strict type checking - Build a generic container class with proper type parameters
Create a type-safe data processing pipeline that:
- Accepts various input types (CSV, JSON, dict)
- Processes data with type-checked transformations
- Returns typed results with metadata
- Uses Protocols for pluggable processors
- Includes comprehensive type hints and passes
mypy --strict