Skip to main content

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}!"
Note

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
  • async and await keywords
  • Concurrent operations
  • Building async applications

Practice Exercises

  1. Add type hints to an existing Python project
  2. Create a typed data class for a complex domain model
  3. Write a type-safe API client with proper annotations
  4. Configure mypy for strict type checking
  5. Build a generic container class with proper type parameters
Challenge

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