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! 🎯