Giao diện
Type Hinting — Hệ thống kiểu tĩnh cho Python
Một đồng nghiệp push một function nhận data và trả về result. Không type annotation, không docstring. Bạn đọc code 15 phút, trace qua 4 file, và vẫn không biết data là dict, list, hay một custom object nào đó. Đây không phải tình huống giả định — đây là lý do Dropbox đã invest vào mypy (Jukka Lehtosalo tạo mypy trong luận văn tiến sĩ, sau đó Dropbox tuyển anh ấy và bắt buộc type hints trên toàn bộ codebase).
Google dùng pytype, Meta dùng Pyre, Microsoft dùng Pyright. Tất cả Big Tech đều đi đến cùng kết luận: Python động kiểu thì linh hoạt, nhưng khi codebase vượt 50K dòng, bạn cần static analysis để ngăn chặn các lỗi mà unit tests không cover hết. Type hints không làm code chạy nhanh hơn — chúng làm developer nhanh hơn.
Bài này bắt đầu từ annotations cơ bản, đi qua generics với cú pháp Python 3.12, rồi nâng lên Protocol, TypedDict, và cấu hình mypy/pyright cho project thực tế.
Bức tranh tư duy
Hãy nghĩ về type hints như bản vẽ kỹ thuật của một tòa nhà.
Tòa nhà vẫn đứng vững mà không cần bản vẽ — giống như Python vẫn chạy mà không cần type hints. Nhưng khi bạn muốn sửa ống nước tầng 3 mà không có bản vẽ, bạn phải đục tường ra xem ống nào dẫn đi đâu. Với bản vẽ (type hints), bạn nhìn thấy ngay: đường ống này (function) nhận nước từ bể chứa A (tham số kiểu UserDict), xử lý qua bộ lọc B (logic), và đổ ra vòi C (trả về list[User]).
Bản vẽ không ngăn bạn xây sai — nhưng nó giúp nhà thầu khác (mypy/pyright) kiểm tra xem bạn có đang nối ống nước vào đường điện không. Và quan trọng hơn, nó giúp kỹ sư mới vào dự án hiểu hệ thống trong vài giờ thay vì vài tuần.
Điều quan trọng cần ghi nhớ: type hints trong Python là metadata — CPython hoàn toàn bỏ qua chúng lúc runtime. Chúng chỉ có ý nghĩa với static type checker (mypy, pyright) và với con người đọc code.
Cốt lõi kỹ thuật
Annotations cơ bản
Biến và function annotations là điểm xuất phát. Python 3.9+ cho phép dùng built-in types trực tiếp làm generic — không cần import từ typing.
python
# Biến và hàm cơ bản (Python 3.9+)
name: str = "Penalgo"
port: int = 8080
active: bool = True
def calculate_tax(income: float, rate: float = 0.1) -> float:
return income * rate
def log_event(message: str, level: str = "INFO") -> None:
print(f"[{level}] {message}")Collections và Optional
python
# Collections (Python 3.9+) — không cần from typing import List, Dict
users: list[str] = ["alice", "bob"]
scores: dict[str, float] = {"alice": 95.0}
point: tuple[float, float] = (10.5, 20.3)
unique_ids: set[int] = {1, 2, 3}
# Optional — giá trị có thể là None (Python 3.10+)
def find_user(user_id: int) -> dict[str, str] | None:
"""Trả về user dict hoặc None nếu không tìm thấy."""
return db.get(user_id)
# Union — nhiều kiểu (Python 3.10+)
def parse_input(value: str | int | float) -> str:
return str(value)TypedDict — Dict có schema cố định
Khi bạn làm việc với JSON API responses hoặc config dicts, TypedDict cho phép định nghĩa chính xác các key và kiểu giá trị của chúng.
python
from typing import TypedDict, NotRequired
class OrderPayload(TypedDict):
order_id: str
customer_email: str
total: float
discount_code: NotRequired[str] # trường tùy chọn
def process_order(payload: OrderPayload) -> None:
# Type checker biết payload["order_id"] là str
print(f"Processing {payload['order_id']}")
# ✅ Hợp lệ
process_order({"order_id": "ORD-1", "customer_email": "a@b.com", "total": 100.0})
# ❌ mypy error: thiếu 'total'
process_order({"order_id": "ORD-1", "customer_email": "a@b.com"})Literal và Annotated
python
from typing import Literal, Annotated
# Literal — giới hạn giá trị cụ thể
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
print(f"Log level: {level}")
set_log_level("INFO") # ✅
set_log_level("VERBOSE") # ❌ mypy error: not a valid literal
# Annotated — gắn metadata vào type (hữu ích với Pydantic, FastAPI)
from annotated_types import Gt, Lt
Price = Annotated[float, Gt(0), Lt(1_000_000)]
def create_product(name: str, price: Price) -> None:
... # Pydantic sẽ validate price > 0 và < 1,000,000 tại runtimeGenerics với Python 3.12 syntax
Python 3.12 thay đổi căn bản cách viết generics. Không cần TypeVar riêng, không cần Generic[T] — mọi thứ inline và rõ ràng hơn.
python
# === Cũ (Python 3.11 trở về) ===
from typing import TypeVar, Generic, Sequence
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()
def first(items: Sequence[T]) -> T:
return items[0]
# === Mới (Python 3.12+) ===
class Stack[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 first[T](items: Sequence[T]) -> T:
return items[0]
# Bounded type parameter
def double[T: (int, float)](x: T) -> T:
return x * 2 # type checker biết T chỉ là int hoặc float
# Generic type alias (Python 3.12+)
type ListOrSet[T] = list[T] | set[T]
type Callback[T] = Callable[[T], None]Protocol — Structural typing
Protocol cho phép định nghĩa interface mà không cần kế thừa. Bất kỳ class nào có đủ các method/attribute được khai báo trong Protocol đều "thỏa mãn" kiểu đó — giống duck typing nhưng với static checking.
python
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_json(self) -> str: ...
class User:
def __init__(self, name: str) -> None:
self.name = name
def to_json(self) -> str:
return f'{{"name": "{self.name}"}}'
class Product:
def __init__(self, title: str, price: float) -> None:
self.title = title
self.price = price
def to_json(self) -> str:
return f'{{"title": "{self.title}", "price": {self.price}}}'
def save_to_cache(obj: Serializable) -> None:
"""Chấp nhận bất kỳ object nào có method to_json()."""
cache.set(obj.to_json())
save_to_cache(User("Alice")) # ✅ User có to_json()
save_to_cache(Product("X", 10)) # ✅ Product có to_json()
save_to_cache(42) # ❌ mypy error: int không có to_json()ParamSpec — Bảo toàn signature qua decorator
python
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec('P')
R = TypeVar('R')
# === Cũ: decorator làm mất type information ===
def bad_timer(func: Callable[..., object]) -> Callable[..., object]:
def wrapper(*args: object, **kwargs: object) -> object:
import time
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
return result
return wrapper
# === Tốt: ParamSpec giữ nguyên signature ===
def timer(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
import time
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
return result
return wrapper
# Python 3.12+ syntax
def timer_312[**P, R](func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
import time
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timer
def fetch_user(user_id: int, include_posts: bool = False) -> dict:
...
# Type checker giữ nguyên: fetch_user(user_id: int, include_posts: bool = False) -> dictoverload — Nhiều signature cho một function
python
from typing import overload
@overload
def parse(data: str) -> dict: ...
@overload
def parse(data: bytes) -> dict: ...
@overload
def parse(data: str, raw: Literal[True]) -> str: ...
def parse(data: str | bytes, raw: bool = False) -> dict | str:
text = data if isinstance(data, str) else data.decode()
if raw:
return text
import json
return json.loads(text)
# Type checker biết:
result1 = parse("{}" ) # -> dict
result2 = parse(b"{}" ) # -> dict
result3 = parse("{}", raw=True ) # -> strThực chiến
Tình huống 1: Gradual typing cho legacy codebase
Bạn vừa nhận maintain một project Django 80K dòng, không có một dòng type hint nào. Không thể thêm type hints cho toàn bộ cùng lúc. Chiến lược gradual typing:
ini
# mypy.ini — bước 1: bật strict cho code mới, lỏng cho code cũ
[mypy]
python_version = 3.12
strict = false
warn_return_any = true
show_error_codes = true
# Code mới: strict
[mypy-src.api.*]
disallow_untyped_defs = true
disallow_incomplete_defs = true
# Code cũ: chỉ warning
[mypy-src.legacy.*]
ignore_errors = true
# Third-party không có stubs
[mypy-some_old_lib.*]
ignore_missing_imports = truepython
# Bước 2: bắt đầu từ "ranh giới" — function signatures của API endpoints
# Đây là nơi type hints mang lại giá trị lớn nhất với nỗ lực nhỏ nhất
# Trước (không ai biết data là gì)
def create_order(data, user):
...
# Sau (rõ ràng, IDE autocomplete hoạt động)
def create_order(data: OrderPayload, user: AuthenticatedUser) -> OrderResponse:
...Tình huống 2: Generic repository pattern
python
from typing import Protocol, TypeVar
class HasId(Protocol):
id: int
T = TypeVar('T', bound=HasId)
class Repository[T: HasId]:
"""Generic repository với type safety."""
def __init__(self, model_class: type[T]) -> None:
self._model = model_class
self._store: dict[int, T] = {}
def get(self, entity_id: int) -> T | None:
return self._store.get(entity_id)
def save(self, entity: T) -> None:
self._store[entity.id] = entity
def list_all(self) -> list[T]:
return list(self._store.values())
# Sử dụng
class User:
def __init__(self, id: int, name: str) -> None:
self.id = id
self.name = name
class Product:
def __init__(self, id: int, title: str, price: float) -> None:
self.id = id
self.title = title
self.price = price
user_repo = Repository(User)
product_repo = Repository(Product)
user_repo.save(User(1, "Alice"))
product_repo.save(Product(1, "Widget", 9.99))
user = user_repo.get(1) # type: User | None
product = product_repo.get(1) # type: Product | NoneSai lầm điển hình
Sai lầm 1: Dùng Any như escape hatch
python
# SAI: Any vô hiệu hóa toàn bộ type checking
from typing import Any
def process(data: Any) -> Any:
return data.foo.bar() # Không lỗi type, nhưng crash lúc runtime
# ĐÚNG: Dùng `object` hoặc Protocol cụ thể
def process(data: object) -> str:
if not hasattr(data, 'foo'):
raise TypeError("data must have 'foo' attribute")
...Sai lầm 2: Nhầm type hints là runtime validation
python
# SAI: Nghĩ rằng type hint sẽ ngăn giá trị sai
def set_age(age: int) -> None:
print(f"Age: {age}")
set_age("twenty") # Python vẫn chạy bình thường! Không lỗi runtime.
# ĐÚNG: Type hints cho static checker, validation cho runtime
def set_age(age: int) -> None:
if not isinstance(age, int):
raise TypeError(f"age must be int, got {type(age).__name__}")
print(f"Age: {age}")
# Hoặc dùng Pydantic cho automatic runtime validation
from pydantic import BaseModel
class UserInput(BaseModel):
age: int # Pydantic validate lúc runtimeSai lầm 3: Bỏ qua None trong return type
python
# SAI: Function có thể trả về None nhưng không khai báo
def find_user(user_id: int) -> dict:
result = db.query(user_id)
return result # có thể là None!
user = find_user(999)
print(user["name"]) # Runtime crash: TypeError: 'NoneType' is not subscriptable
# ĐÚNG: Khai báo Optional và xử lý None
def find_user(user_id: int) -> dict | None:
return db.query(user_id)
user = find_user(999)
if user is not None:
print(user["name"]) # Type checker happy, runtime safeSai lầm 4: Quên forward references
python
# SAI: Class chưa được định nghĩa tại thời điểm sử dụng
class TreeNode:
def __init__(self, children: list[TreeNode]) -> None: # NameError!
self.children = children
# ĐÚNG CÁCH 1: String annotation
class TreeNode:
def __init__(self, children: list["TreeNode"]) -> None:
self.children = children
# ĐÚNG CÁCH 2: from __future__ (PEP 563)
from __future__ import annotations
class TreeNode:
def __init__(self, children: list[TreeNode]) -> None: # OK!
self.children = childrenSai lầm 5: Nhầm variance của container types
python
# SAI: list là invariant, không phải covariant
def feed_animals(animals: list[Animal]) -> None:
animals.append(Cat()) # Đây là lý do list invariant
dogs: list[Dog] = [Dog()]
feed_animals(dogs) # mypy error! Nếu cho phép, dogs sẽ chứa Cat
# ĐÚNG: Dùng Sequence (covariant, read-only) khi chỉ đọc
from collections.abc import Sequence
def feed_animals(animals: Sequence[Animal]) -> None:
for a in animals:
a.eat() # Chỉ đọc, không append
dogs: list[Dog] = [Dog()]
feed_animals(dogs) # ✅ Sequence[Dog] compatible với Sequence[Animal]Under the Hood
Type hints không phải runtime
Điểm then chốt nhất: CPython hoàn toàn bỏ qua type hints lúc runtime. Annotations được lưu trong attribute __annotations__ của function/class, nhưng không được enforce.
python
def greet(name: str) -> str:
return f"Hello {name}"
print(greet.__annotations__)
# {'name': <class 'str'>, 'return': <class 'str'>}
greet(42) # Chạy bình thường, không lỗi!Từ Python 3.13, PEP 649 thay đổi cách annotations được evaluate: thay vì evaluate ngay lúc định nghĩa (eager), chúng sẽ được evaluate lười (lazy) khi truy cập __annotations__. Điều này giải quyết vấn đề forward references mà không cần from __future__ import annotations.
Lịch sử PEP về typing
| PEP | Phiên bản | Nội dung chính |
|---|---|---|
| PEP 484 | 3.5 | Type hints cơ bản, typing module |
| PEP 526 | 3.6 | Variable annotations (x: int = 1) |
| PEP 544 | 3.8 | Protocol (structural subtyping) |
| PEP 585 | 3.9 | list[int] thay vì List[int] |
| PEP 604 | 3.10 | X | Y thay vì Union[X, Y] |
| PEP 612 | 3.10 | ParamSpec cho decorator typing |
| PEP 695 | 3.12 | Type parameter syntax (class Foo[T]) |
| PEP 649 | 3.14 | Deferred evaluation of annotations |
mypy vs pyright — trade-offs
mypy là reference implementation, được Dropbox sponsor. Nhược điểm: chậm hơn pyright đáng kể trên codebase lớn. Ưu điểm: hệ sinh thái plugin phong phú (Pydantic, SQLAlchemy, Django).
pyright (Microsoft) được viết bằng TypeScript, chạy nhanh hơn mypy 5–10 lần trên cùng codebase. Tích hợp tốt với VS Code (Pylance). Strict mode của pyright khắt khe hơn mypy strict.
Cả hai đều tuân theo PEP chuẩn, nhưng có sự khác biệt nhỏ trong cách xử lý edge cases. Trong production, nhiều team chạy cả hai trong CI.
Cấu hình production với pyproject.toml
toml
[tool.mypy]
python_version = "3.12"
strict = true
show_error_codes = true
pretty = true
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "migrations.*"
ignore_errors = truejson
// pyrightconfig.json
{
"include": ["src"],
"pythonVersion": "3.12",
"typeCheckingMode": "strict",
"reportMissingTypeStubs": false,
"reportUnusedImport": true
}Checklist ghi nhớ
✅ Checklist triển khai
Annotations cơ bản
- [ ] Dùng cú pháp Python 3.10+ (
X | Ythay vìUnion[X, Y]) - [ ] Mọi function đều có return type annotation
- [ ] Khai báo
-> Nonecho function không trả về gì
Generics và Protocol
- [ ] Dùng Python 3.12 type parameter syntax (
class Foo[T]) khi có thể - [ ] Dùng Protocol cho structural typing thay vì ABC khi không cần runtime check
- [ ] Dùng
Sequence(covariant) thay vìlist(invariant) khi chỉ đọc
Safety
- [ ] Không dùng
Anytrừ khi bắt buộc (và comment lý do) - [ ] Mọi giá trị có thể None đều khai báo
| None - [ ] Dùng
TypeGuardcho custom type narrowing functions - [ ] Dùng
overloadkhi function trả về kiểu khác nhau tùy input
Tooling
- [ ] Cấu hình mypy hoặc pyright trong CI pipeline
- [ ] Bật strict mode cho code mới, gradual cho code cũ
- [ ] Pre-commit hook chạy type checker trước mỗi commit
Bài tập luyện tập
Bài 1: Typed data pipeline (Foundation)
Viết một module xử lý dữ liệu với đầy đủ type annotations. Module nhận vào một list các dict (dạng UserRecord), lọc theo điều kiện, và trả về kết quả đã transform.
Yêu cầu: định nghĩa TypedDict cho input/output, function signatures đầy đủ annotations, pass mypy --strict.
Gợi ý
python
from typing import TypedDict
class UserRecord(TypedDict):
id: int
name: str
email: str
active: bool
class UserSummary(TypedDict):
id: int
name: str
def filter_active(users: list[UserRecord]) -> list[UserRecord]:
return [u for u in users if u["active"]]
def to_summary(users: list[UserRecord]) -> list[UserSummary]:
return [{"id": u["id"], "name": u["name"]} for u in users]
def pipeline(raw_users: list[UserRecord]) -> list[UserSummary]:
active = filter_active(raw_users)
return to_summary(active)Bài 2: Generic cache với TTL (Intermediate)
Implement một TTLCache[K, V] generic class. Cache lưu cặp key-value với thời gian hết hạn. Method get trả về V | None. Dùng Python 3.12 type parameter syntax.
Gợi ý
python
import time
from typing import Hashable
class TTLCache[K: Hashable, V]:
def __init__(self, ttl_seconds: float) -> None:
self._ttl = ttl_seconds
self._store: dict[K, tuple[V, float]] = {}
def set(self, key: K, value: V) -> None:
self._store[key] = (value, time.monotonic())
def get(self, key: K) -> V | None:
entry = self._store.get(key)
if entry is None:
return None
value, timestamp = entry
if time.monotonic() - timestamp > self._ttl:
del self._store[key]
return None
return value🧠 Quiz
Type hints trong Python được enforce lúc nào?
- [ ] Lúc import module
- [ ] Lúc gọi function
- [x] Chỉ lúc chạy static type checker (mypy, pyright)
- [ ] Lúc compile bytecode
Giải thích: CPython hoàn toàn bỏ qua type hints lúc runtime. Annotations được lưu trong __annotations__ nhưng không được kiểm tra. Chỉ có static type checkers mới phân tích chúng.
🧠 Quiz
Tại sao list[Dog] không thể truyền vào list[Animal]?
- [ ] Vì Dog không kế thừa Animal
- [x] Vì
listlà invariant — cho phép sẽ gây bug khi append - [ ] Vì Python không hỗ trợ subtyping
- [ ] Vì cần dùng cast()
Giải thích: Nếu list[Dog] được coi là list[Animal], bạn có thể append(Cat()) vào danh sách dogs. Dùng Sequence[Animal] (covariant, read-only) để an toàn.
Liên kết học tiếp
Từ khóa: type hints, annotations, generics, TypeVar, ParamSpec, Protocol, TypedDict, Literal, Annotated, overload, mypy, pyright, static type checking, PEP 484, PEP 695