Bài 12: Type Hints và Annotations
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Sử dụng type hints syntax
- ✅ Làm việc với typing module
- ✅ Tạo function annotations
- ✅ Sử dụng mypy static type checker
- ✅ Hiểu Protocol và TypedDict
- ✅ Áp dụng best practices
Type Hints Basics
Type hints cho phép specify types của variables, parameters, và return values.
Why Type Hints?
# Without type hints - uncleardef greet(name): return f"Hello, {name}" # With type hints - clear expectationsdef greet(name: str) -> str: return f"Hello, {name}" # Benefits:# 1. Better IDE autocomplete# 2. Catch bugs early with static analysis# 3. Self-documenting code# 4. Easier refactoring
Basic Type Annotations
# Variablesage: int = 25name: str = "Alice"price: float = 99.99is_active: bool = True # Function parameters and return typedef add(a: int, b: int) -> int: return a + b def get_greeting(name: str) -> str: return f"Hello, {name}" # No return valuedef print_message(message: str) -> None: print(message) # Multiple parametersdef calculate_discount(price: float, discount: float) -> float: return price * (1 - discount) print(add(5, 3)) # 8print(get_greeting("Bob")) # Hello, Bob
typing Module
Module typing cung cấp advanced type hints.
List, Dict, Tuple, Set
from typing import List, Dict, Tuple, Set # List of integersnumbers: List[int] = [1, 2, 3, 4, 5] # Dict with string keys and int valuesscores: Dict[str, int] = {"Alice": 95, "Bob": 87} # Tuple with specific typescoordinates: Tuple[float, float] = (10.5, 20.3) # Set of stringstags: Set[str] = {"python", "programming", "tutorial"} # Function with complex typesdef get_user_scores(users: List[str]) -> Dict[str, int]: return {user: 0 for user in users} # Nested typesmatrix: List[List[int]] = [[1, 2, 3], [4, 5, 6]]user_data: Dict[str, List[str]] = { "Alice": ["admin", "user"], "Bob": ["user"]}
Optional and Union
from typing import Optional, Union # Optional - value or Nonedef find_user(user_id: int) -> Optional[str]: users = {1: "Alice", 2: "Bob"} return users.get(user_id) # Can return str or None # Same as Union[str, None]def get_name() -> Optional[str]: return None # Union - multiple typesdef process_id(id_value: Union[int, str]) -> str: return str(id_value) print(process_id(123)) # "123"print(process_id("abc")) # "abc" # Multiple unionsdef format_value(value: Union[int, float, str]) -> str: return str(value) # Optional with defaultdef greet(name: Optional[str] = None) -> str: if name is None: return "Hello, Guest" return f"Hello, {name}"
Any and TypeVar
from typing import Any, TypeVar # Any - any type (avoid when possible)def process_data(data: Any) -> Any: return data # TypeVar - generic typesT = TypeVar('T') def first_element(items: List[T]) -> T: return items[0] # Works with any typeprint(first_element([1, 2, 3])) # 1 (int)print(first_element(["a", "b", "c"])) # "a" (str) # Generic functiondef reverse_list(items: List[T]) -> List[T]: return items[::-1] # Type is preservednumbers = reverse_list([1, 2, 3]) # List[int]words = reverse_list(["a", "b"]) # List[str]
Callable
from typing import Callable # Function that takes function as parameterdef apply_operation( x: int, y: int, operation: Callable[[int, int], int]) -> int: return operation(x, y) def add(a: int, b: int) -> int: return a + b def multiply(a: int, b: int) -> int: return a * b print(apply_operation(5, 3, add)) # 8print(apply_operation(5, 3, multiply)) # 15 # Callback typeCallback = Callable[[str], None] def process_data(data: str, callback: Callback) -> None: # Process data callback(f"Processed: {data}") def print_result(result: str) -> None: print(result) process_data("test", print_result)
Advanced Type Hints
TypedDict
from typing import TypedDict # Define structure of dictclass UserDict(TypedDict): id: int name: str email: str active: bool # Use typed dictdef create_user(name: str, email: str) -> UserDict: return { "id": 1, "name": name, "email": email, "active": True } # Type checker will catch errorsuser: UserDict = create_user("Alice", "[email protected]")print(user["name"]) # OK# print(user["invalid"]) # Type checker error # Optional fieldsclass ConfigDict(TypedDict, total=False): host: str port: int debug: bool # Optional config: ConfigDict = {"host": "localhost"} # OK, port not required
Protocol (Structural Subtyping)
from typing import Protocol # Define protocol (interface)class Drawable(Protocol): def draw(self) -> None: ... # Classes that implement draw() are compatibleclass Circle: def draw(self) -> None: print("Drawing circle") class Square: def draw(self) -> None: print("Drawing square") def render(shape: Drawable) -> None: shape.draw() # Works without inheritancerender(Circle()) # Drawing circlerender(Square()) # Drawing square # More complex protocolclass Comparable(Protocol): def __lt__(self, other: "Comparable") -> bool: ... def find_min(items: List[Comparable]) -> Comparable: return min(items)
Literal and Final
from typing import Literal, Final # Literal - specific values onlydef set_mode(mode: Literal["dev", "prod", "test"]) -> None: print(f"Mode: {mode}") set_mode("dev") # OK# set_mode("invalid") # Type error # Final - constant valueMAX_SIZE: Final = 100# MAX_SIZE = 200 # Type error class Config: MAX_CONNECTIONS: Final[int] = 10 # Cannot reassign in subclass
Class Type Hints
from typing import List, Optional, ClassVar class User: # Class variable count: ClassVar[int] = 0 def __init__(self, name: str, age: int) -> None: self.name: str = name self.age: int = age User.count += 1 def get_info(self) -> str: return f"{self.name}, {self.age} years old" @staticmethod def create_guest() -> "User": # Forward reference return User("Guest", 0) @classmethod def from_dict(cls, data: dict) -> "User": return cls(data["name"], data["age"]) # Generic classfrom typing import Generic, TypeVar 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) -> Optional[T]: if self.items: return self.items.pop() return None def is_empty(self) -> bool: return len(self.items) == 0 # Type-specific stacksint_stack: Stack[int] = Stack()str_stack: Stack[str] = Stack() int_stack.push(1)str_stack.push("hello")
mypy - Static Type Checker
Installation and Usage
# Install mypypip install mypy # Check filemypy script.py # Check entire projectmypy . # With configmypy --config-file mypy.ini .
Example Code
# example.pydef add(a: int, b: int) -> int: return a + b def greet(name: str) -> str: return f"Hello, {name}" # Correct usageresult = add(5, 3)message = greet("Alice") # Type errors (mypy will catch)# result = add("5", "3") # Error: Expected int# message = greet(123) # Error: Expected str
mypy Configuration
# mypy.ini[mypy]python_version = 3.11warn_return_any = Truewarn_unused_configs = Truedisallow_untyped_defs = Truedisallow_any_explicit = Falsedisallow_any_generics = Falsedisallow_subclassing_any = False
Real-world Examples
1. API Response Handler
from typing import TypedDict, Optional, List class APIResponse(TypedDict): status: int data: Optional[dict] message: str class UserData(TypedDict): id: int username: str email: str def fetch_user(user_id: int) -> APIResponse: """Fetch user from API.""" # Simulated response return { "status": 200, "data": { "id": user_id, "username": "alice", "email": "[email protected]" }, "message": "Success" } def parse_users(responses: List[APIResponse]) -> List[UserData]: """Parse user data from responses.""" users: List[UserData] = [] for response in responses: if response["status"] == 200 and response["data"]: users.append({ "id": response["data"]["id"], "username": response["data"]["username"], "email": response["data"]["email"] }) return users
2. Configuration Manager
from typing import TypedDict, Optional, Union class DatabaseConfig(TypedDict): host: str port: int database: str username: str password: str class AppConfig(TypedDict): debug: bool database: DatabaseConfig secret_key: str class ConfigManager: def __init__(self, config: AppConfig) -> None: self.config = config def get(self, key: str, default: Optional[str] = None) -> Union[str, bool, DatabaseConfig]: return self.config.get(key, default) # type: ignore def is_debug(self) -> bool: return self.config["debug"] def get_db_config(self) -> DatabaseConfig: return self.config["database"] # Usage with type safetyconfig: AppConfig = { "debug": True, "database": { "host": "localhost", "port": 5432, "database": "mydb", "username": "user", "password": "pass" }, "secret_key": "secret"} manager = ConfigManager(config)print(manager.is_debug())
3. Data Validator
from typing import List, Dict, Any, Optional, Callable ValidatorFunc = Callable[[Any], bool] class Validator: def __init__(self) -> None: self.rules: Dict[str, List[ValidatorFunc]] = {} def add_rule(self, field: str, validator: ValidatorFunc) -> None: """Add validation rule for field.""" if field not in self.rules: self.rules[field] = [] self.rules[field].append(validator) def validate(self, data: Dict[str, Any]) -> Dict[str, List[str]]: """Validate data and return errors.""" errors: Dict[str, List[str]] = {} for field, validators in self.rules.items(): if field not in data: errors[field] = ["Field is required"] continue field_errors: List[str] = [] for validator in validators: if not validator(data[field]): field_errors.append(f"Validation failed for {field}") if field_errors: errors[field] = field_errors return errors # Usagevalidator = Validator()validator.add_rule("age", lambda x: isinstance(x, int) and x >= 0)validator.add_rule("email", lambda x: isinstance(x, str) and "@" in x) data = {"age": 25, "email": "[email protected]"}errors = validator.validate(data)print(errors) # {}
4. Generic Repository
from typing import Generic, TypeVar, List, Optional, Protocol T = TypeVar('T') class Entity(Protocol): id: int class Repository(Generic[T]): def __init__(self) -> None: self.items: List[T] = [] def add(self, item: T) -> None: self.items.append(item) def get_by_id(self, id: int) -> Optional[T]: for item in self.items: if hasattr(item, 'id') and item.id == id: # type: ignore return item return None def get_all(self) -> List[T]: return self.items.copy() def delete(self, id: int) -> bool: for i, item in enumerate(self.items): if hasattr(item, 'id') and item.id == id: # type: ignore del self.items[i] return True return False # Usage with specific typesclass User: def __init__(self, id: int, name: str) -> None: self.id = id self.name = name user_repo: Repository[User] = Repository()user_repo.add(User(1, "Alice"))user = user_repo.get_by_id(1)
5. Async Type Hints
from typing import List, Coroutine, Anyimport asyncio async def fetch_data(url: str) -> dict: """Fetch data from URL.""" await asyncio.sleep(1) return {"url": url, "status": 200} async def fetch_all(urls: List[str]) -> List[dict]: """Fetch multiple URLs concurrently.""" tasks: List[Coroutine[Any, Any, dict]] = [ fetch_data(url) for url in urls ] results = await asyncio.gather(*tasks) return results # Type hints for async functionsasync def process() -> None: urls = ["url1", "url2", "url3"] results = await fetch_all(urls) print(results)
Best Practices
from typing import Optional, List, Dict, Union # 1. Use type hints for all function signaturesdef calculate(x: int, y: int) -> int: return x + y # 2. Use Optional for nullable valuesdef find_user(id: int) -> Optional[str]: return None # 3. Be specific with container typesdef process_names(names: List[str]) -> Dict[str, int]: return {name: len(name) for name in names} # 4. Use TypedDict for structured dictsfrom typing import TypedDict class UserDict(TypedDict): id: int name: str # 5. Use Protocol for duck typingfrom typing import Protocol class Drawable(Protocol): def draw(self) -> None: ... # 6. Use generics for reusable codefrom typing import TypeVar, Generic T = TypeVar('T') class Container(Generic[T]): def __init__(self, value: T) -> None: self.value = value # 7. Run mypy regularly# mypy --strict your_code.py # 8. Start with simple types, add gradually# Don't try to type everything at once
Bài Tập Thực Hành
Bài 1: API Client
Tạo typed API client:
- Request/response types
- TypedDict for payloads
- Generic methods
- Error types
Bài 2: Data Processor
Build data processor:
- Generic processing pipeline
- Type-safe transformations
- Validator với type hints
- Protocol for extensibility
Bài 3: ORM-like System
Create simple ORM:
- Generic repository
- Type-safe queries
- Model definitions
- Relationship types
Bài 4: Config System
Build config manager:
- TypedDict for config
- Validation with types
- Multiple sources
- Type-safe access
Bài 5: Plugin System
Implement plugin system:
- Protocol for plugins
- Generic plugin manager
- Type-safe registration
- Dependency injection
Tóm Tắt
✅ Type hints: Better IDE support, catch bugs early
✅ Basic types: int, str, float, bool, None
✅ typing module: List, Dict, Optional, Union, Callable
✅ TypedDict: Structured dict types
✅ Protocol: Structural subtyping (duck typing)
✅ Generic: Reusable type-safe code
✅ mypy: Static type checker tool
✅ Best practices: Start simple, add gradually
Bài Tiếp Theo
Bài 13: Testing - unittest, pytest, fixtures, mocking, và TDD basics! 🚀
Remember:
- Type hints improve code quality
- Use mypy for static checking
- Start with function signatures
- TypedDict for structured data
- Protocol for duck typing! 🎯