Making Python Code More Robust
Benefits:
Using typing module for complex types:
from typing import List, Dict, Tuple, Set, Optional
# 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": "8080"}
# Tuples
coordinates: Tuple[float, float] = (10.5, 20.3)
rgb_color: Tuple[int, int, int] = (255, 128, 0)
# Sets
unique_ids: Set[int] = {1, 2, 3, 4, 5}Optional types for nullable values:
from typing import Optional, Union
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)
# Optional[str] is equivalent to Union[str, None]
def get_config_value(key: str) -> Union[str, None]:
return config.get(key)Callable types:
from typing import Callable
def apply_operation(x: int, y: int, operation: Callable[[int, int], int]) -> int:
"""Apply an operation function to two integers."""
return operation(x, y)
def add(a: int, b: int) -> int:
return a + b
def multiply(a: int, b: int) -> int:
return a * b
result1 = apply_operation(5, 3, add) # 8
result2 = apply_operation(5, 3, multiply) # 15Create flexible, reusable type hints:
from typing import TypeVar, Generic, List
T = TypeVar('T') # Type variable
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()
def is_empty(self) -> bool:
return len(self._items) == 0
# Usage with specific types
int_stack: Stack[int] = Stack()
str_stack: Stack[str] = Stack()
int_stack.push(42)
str_stack.push("hello")Annotating class attributes and methods:
from typing import List, Optional
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
grades: List[float]
email: Optional[str] = None
def add_grade(self, grade: float) -> None:
self.grades.append(grade)
def get_average(self) -> float:
if not self.grades:
return 0.0
return sum(self.grades) / len(self.grades)
def get_letter_grade(self) -> str:
avg = self.get_average()
if avg >= 90:
return "A"
elif avg >= 80:
return "B"
elif avg >= 70:
return "C"
else:
return "F"Define interfaces without inheritance:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
def draw(self) -> None:
print(f"Drawing circle with radius {self.radius}")
class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def draw(self) -> None:
print(f"Drawing rectangle {self.width}x{self.height}")
def render_shape(shape: Drawable) -> None:
shape.draw() # Works with any object that has draw() method
# Both work due to structural typing
render_shape(Circle(5))
render_shape(Rectangle(10, 5))Example file: example.py
Literal types for specific values:
Create readable aliases for complex types:
from typing import Dict, List, Tuple
# Define type aliases
UserId = int
UserName = str
Coordinates = Tuple[float, float]
UserData = Dict[UserId, UserName]
Point = Tuple[float, float]
Polygon = List[Point]
def get_user_info(user_id: UserId) -> UserName:
users: UserData = {1: "alice", 2: "bob"}
return users.get(user_id, "unknown")
def calculate_area(polygon: Polygon) -> float:
# Complex area calculation
return 0.0 # Simplifiedfrom typing import Optional, Dict, Any, List, Union
from dataclasses import dataclass
import requests
@dataclass
class APIResponse:
status_code: int
data: Optional[Dict[str, Any]]
error: Optional[str] = None
class APIClient:
def __init__(self, base_url: str, api_key: str) -> None:
self.base_url = base_url
self.api_key = api_key
self.headers: Dict[str, str] = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def get(self, endpoint: str, params: Optional[Dict[str, Union[str, int]]] = None) -> APIResponse:
"""Make GET request to API endpoint."""
url = f"{self.base_url}/{endpoint}"
try:
response = requests.get(url, headers=self.headers, params=params)
return APIResponse(
status_code=response.status_code,
data=response.json() if response.ok else None,
error=None if response.ok else response.text
)
except Exception as e:
return APIResponse(
status_code=500,
data=None,
error=str(e)
)
def post(self, endpoint: str, data: Dict[str, Any]) -> APIResponse:
"""Make POST request to API endpoint."""
url = f"{self.base_url}/{endpoint}"
try:
response = requests.post(url, headers=self.headers, json=data)
return APIResponse(
status_code=response.status_code,
data=response.json() if response.ok else None,
error=None if response.ok else response.text
)
except Exception as e:
return APIResponse(
status_code=500,
data=None,
error=str(e)
)Challenge 1: Banking System
from typing import Optional, List
from dataclasses import dataclass
@dataclass
class Transaction:
# Add type hints
pass
class BankAccount:
def __init__(self, account_number, balance):
# Add type hints
pass
def deposit(self, amount):
# Add type hints and implementation
pass
def withdraw(self, amount):
# Add type hints and implementation
pass
def get_balance(self):
# Add type hints
passBanking System with Type Hints:
from typing import Optional, List
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Transaction:
transaction_type: str
amount: float
timestamp: datetime
description: Optional[str] = None
class BankAccount:
def __init__(self, account_number: str, balance: float = 0.0) -> None:
self.account_number = account_number
self.balance = balance
self.transactions: List[Transaction] = []
def deposit(self, amount: float, description: Optional[str] = None) -> bool:
if amount > 0:
self.balance += amount
self.transactions.append(
Transaction("deposit", amount, datetime.now(), description)
)
return True
return False
def withdraw(self, amount: float, description: Optional[str] = None) -> bool:
if 0 < amount <= self.balance:
self.balance -= amount
self.transactions.append(
Transaction("withdrawal", amount, datetime.now(), description)
)
return True
return False
def get_balance(self) -> float:
return self.balanceAny1. Overusing Any:
Gradual adoption approach:
Coming up next: - Asynchronous programming with asyncio - Multiprocessing and concurrency - Performance optimization techniques
Practice more: - Add type hints to existing projects - Set up mypy in your development workflow - Create type-safe APIs and data structures
Python Tutorial