- Hiểu type hints là compile-time only: runtime bỏ qua, nhưng tooling đọc.
- Master container generics (
list[T],dict[K, V]) và cú pháp|cho union/None. - Phân biệt
Any,object,Never,Literal,Final. - Sử dụng TypedDict mô tả dict shape JSON, đối chiếu với
@dataclassvàpydantic. - Hiểu Protocol — structural typing duck-typed có check.
- Viết generic function/class với syntax PEP 695 (Python 3.12+).
- Cài và chạy
mypy --strict; biếtPyrightkhác gì. - Tránh anti-pattern over-typing: nơi nào nên annotate, nơi nào để inference.
1. Type hints — recap và sâu hơn
Chương 2 đã giới thiệu cú pháp cơ bản. Ở chương này ta khoét sâu vào ngữ nghĩa. Type hint trong Python được giới thiệu qua PEP 484 (2014) — Guido van Rossum cá nhân thiết kế sau khi thấy mypy của Jukka Lehtosalo đáng giá. Cú pháp lấy cảm hứng từ ML/Haskell nhưng giữ kiểu Python:
# Biến — dấu hai chấm sau tên
x: int = 5
name: str = "An"
active: bool = True
ratio: float = 3.14
# Function — annotate parameter và return
def greet(name: str, age: int) -> str:
return f"Xin chào {name}, {age} tuổi"
# Return None tường minh khi function không trả gì
def log(msg: str) -> None:
print(msg)
# Attribute trong class
class User:
name: str
age: int
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
Khi Python chạy file .py, nó thấy annotation (lưu trong
__annotations__) nhưng không kiểm tra giá trị có khớp không:
def add(a: int, b: int) -> int:
return a + b
add("hello", "world") # 'helloworld' — chạy bình thường!
print(add.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Annotation chỉ trở thành check khi có tool đọc nó: mypy, Pyright,
pyright/Pylance trong VS Code, JetBrains type checker. Hoặc khi dùng thư viện
runtime như pydantic, beartype, typeguard — nhưng phải
chủ động bật.
So sánh nhanh với JavaScript/TypeScript (chương 9 sub-pillar JS): TS có 1 compiler chính thức (tsc),
chạy bắt buộc khi build. Python có nhiều tool độc lập, không bắt buộc — bạn có thể commit code không
type hint, không ai chặn. Đó là điểm yếu (dễ lười) và điểm mạnh (linh hoạt cho script nhỏ).
| Ngôn ngữ | Type system | Tool check | Bắt buộc? |
|---|---|---|---|
| TypeScript | Compile-time, strip khi build | tsc chính thức | Có (file .ts phải pass) |
| Python | Annotation, runtime ignore | mypy, Pyright bên thứ 3 | Không (tự nguyện) |
| Dart | Sound static, runtime check | Compiler tích hợp | Có (sound type system) |
2. Container types — list[T], dict[K,V], tuple
Container generic là nơi type hint thật sự phát huy: thay vì list (list của cái gì?),
ta nói list[int].
nums: list[int] = [1, 2, 3]
names: list[str] = ["An", "Bình"]
ages: dict[str, int] = {"An": 22, "Bình": 30}
# Set
tags: set[str] = {"py", "ts"}
# Tuple — 2 nghĩa khác nhau
# 1) Fixed shape: tuple với type cho từng slot
pair: tuple[str, int] = ("age", 22)
point: tuple[float, float, float] = (1.0, 2.0, 3.0)
# 2) Variable length: tuple[T, ...]
many_ints: tuple[int, ...] = (1, 2, 3, 4)
# Nested
matrix: list[list[int]] = [[1, 2], [3, 4]]
config: dict[str, list[int]] = {"primes": [2, 3, 5]}
typing# Python 3.8 trở xuống (cũ, vẫn còn nhiều code base):
from typing import List, Dict, Tuple, Set
nums: List[int] = [1, 2]
ages: Dict[str, int] = {"An": 22}
# Từ Python 3.9 (PEP 585) — dùng trực tiếp built-in:
nums: list[int] = [1, 2]
ages: dict[str, int] = {"An": 22}
Cú pháp mới ngắn hơn, không cần import. Khuyến nghị dùng built-in nếu project target
Python 3.9+. Nếu phải support 3.8 và sớm hơn: dùng List, Dict từ
typing, hoặc thêm from __future__ import annotations để hoãn evaluation.
3. Optional[T] và T | None
Trong Python None là một giá trị riêng (singleton kiểu NoneType). Khi function có
thể trả None hoặc một type khác, dùng Optional[T] hoặc T | None.
# Cách cũ (Python <3.10) — Optional từ typing
from typing import Optional
def find_user(uid: int) -> Optional[str]:
if uid == 1:
return "An"
return None
# Cách mới (Python 3.10+, PEP 604) — toán tử |
def find_user(uid: int) -> str | None:
if uid == 1:
return "An"
return None
# Hai cái HOÀN TOÀN tương đương — Optional[T] = T | None
T | None
Cú pháp | (PEP 604, 3.10+) đọc tự nhiên hơn, không cần import. Cộng đồng dần chuyển sang nó.
Optional[str] còn dễ gây hiểu lầm: trông như "optional argument", thật ra là "string hoặc None".
Khi muốn parameter optional (có default), vẫn cần = riêng:
# SAI: Optional[int] không làm parameter optional
def f(x: Optional[int]) -> None: ...
f() # TypeError: missing 1 required positional argument
# ĐÚNG: muốn optional thì thêm default
def f(x: int | None = None) -> None: ...
f() # OK
f(5) # OK
f(None) # OK
4. Union[A, B] và A | B
Tổng quát hơn: union nhiều type. Cú pháp cũ Union[A, B, C], mới A | B | C.
from typing import Union
# Cũ
def parse_id(raw: Union[int, str]) -> int:
return int(raw)
# Mới (3.10+)
def parse_id(raw: int | str) -> int:
return int(raw)
# 3 type trở lên
def to_str(x: int | float | str | None) -> str:
if x is None:
return ""
return str(x)
Union khác Optional ở chỗ Optional là một shortcut cho "T hoặc None" — còn union dùng chung cho mọi tổ hợp. Một biến union không thể gọi method được cho đến khi narrow (xem section 14).
5. Any — escape hatch nguy hiểm
Any tương đương any của TypeScript: tắt mọi type check. Bất kỳ phép gán nào,
method call nào đều được phép.
from typing import Any
x: Any = "hello"
x = 42 # OK
x = [1, 2, 3] # OK
x.whatever().chain() # OK ở mypy, 💥 runtime nếu sai
# Any "lây lan": gán biến Any vào biến typed cũng OK
def get_data() -> Any:
return {"k": 1}
n: int = get_data() # OK (Any → int), nhưng runtime n là dict 💥
Any là backdoor. Mỗi Any trong codebase là một điểm không được kiểm tra.
Mypy strict mode (xem section 15) sẽ cảnh báo khi gặp implicit Any.
- Dữ liệu thật sự không biết shape — vd
**kwargstruyền vào library cũ. - Migration JS → TS, Python untyped → typed: tạm
Any, ghi# TODO: type. - Decorator generic phức tạp không thể type chính xác (đang dần được giải bởi
ParamSpec).
Quy tắc: luôn ưu tiên object hoặc Unknown-like pattern nếu được. Với
input không tin được (JSON parse, file read), Python chưa có unknown như TS — pattern phổ
biến là dùng object rồi isinstance để narrow.
6. Never — bottom type
Never (PEP 484, alias NoReturn cho function) đánh dấu một function không bao
giờ trả về: nó throw exception hoặc loop vô hạn.
from typing import Never # Python 3.11+. Trước đó: NoReturn
def fail(msg: str) -> Never:
raise RuntimeError(msg)
def infinite() -> Never:
while True:
do_something()
# Mypy hiểu: dòng sau fail() là "dead code"
def divide(a: int, b: int) -> float:
if b == 0:
fail("chia cho 0")
# Mypy: ở đây code không thể đến được
return a / b
from typing import Never, assert_never
Status = Literal["idle", "loading", "done"]
def handle(s: Status) -> str:
match s:
case "idle": return "chờ"
case "loading": return "đang tải"
case "done": return "xong"
case _:
assert_never(s) # Mypy báo lỗi nếu thêm 'error' vào Status mà quên xử lý
Pattern này giống const _exhaustive: never = s của TypeScript — bắt thiếu case khi thêm
literal mới.
7. Literal — type là giá trị cụ thể
Literal type giới hạn biến chỉ nhận một số giá trị cụ thể. Tương đương union literal của TS
('idle' | 'loading'). Đây là pattern thay Enum trong nhiều trường hợp.
from typing import Literal
Color = Literal["red", "green", "blue"]
def paint(c: Color) -> None:
print(f"Sơn {c}")
paint("red") # OK
paint("yellow") # Mypy: Argument 1 has incompatible type "Literal['yellow']"
# Literal số, bool — cũng được
def flip(side: Literal[0, 1]) -> Literal[0, 1]:
return 1 - side
# Discriminated union — pattern phổ biến
from typing import TypedDict
class SuccessResult(TypedDict):
status: Literal["ok"]
data: str
class ErrorResult(TypedDict):
status: Literal["error"]
message: str
Result = SuccessResult | ErrorResult
def handle(r: Result) -> str:
if r["status"] == "ok":
return r["data"] # Narrow về SuccessResult
else:
return r["message"] # Narrow về ErrorResult
Python Enum (chương 5) là class runtime — có giá trị, có method, có identity. Literal chỉ là
kiểu compile-time, biến mất khi chạy. Quy tắc:
- Literal: khi cần một set giá trị cố định, không cần object riêng. Nhanh, đơn giản.
- Enum: khi cần object — vd
Color.RED.hex(), hoặc serialize sang JSON với tên đẹp. - Đa số case scripting/web — Literal đủ.
8. Final — không reassign
Final đánh dấu biến chỉ gán một lần. Mypy enforce, runtime không.
Tương đương const của TS/JS về mặt ngữ nghĩa compile-time.
from typing import Final
MAX_RETRIES: Final = 3
API_URL: Final[str] = "https://api.example.com"
MAX_RETRIES = 5 # Mypy: Cannot assign to final name "MAX_RETRIES"
# Nhưng runtime vẫn cho phép — Final chỉ là gợi ý cho tool
# Trong class — không cho override ở subclass
class Base:
name: Final[str] = "Base"
class Sub(Base):
name = "Sub" # Mypy: Cannot override writable attribute "name"
Final chỉ chặn reassign tên biến. Nội dung object vẫn mutate được:
USERS: Final[list[str]] = []
USERS.append("An") # OK — list nội dung thay đổi
USERS = [] # Mypy: error — không được gán lại
Để immutable thật: dùng tuple thay list, frozenset thay
set, hoặc @dataclass(frozen=True).
9. TypeAlias — đặt tên cho type
Khi một type phức tạp lặp lại nhiều chỗ, đặt tên cho nó.
# Cũ (Python <3.12) — TypeAlias từ typing
from typing import TypeAlias
UserId: TypeAlias = int
Coord: TypeAlias = tuple[float, float]
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
def find_user(uid: UserId) -> str | None: ...
# Mới (Python 3.12+, PEP 695) — keyword `type`
type UserId = int
type Coord = tuple[float, float]
type JsonValue = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
def find_user(uid: UserId) -> str | None: ...
Cú pháp type mới (PEP 695) ngắn gọn, scope rõ ràng. Lưu ý: không phải chỉ là
alias đồng nhất — type alias mới lazy evaluate, hỗ trợ self-reference tốt hơn.
10. TypedDict — dict có shape
Python rất chuộng dict cho dữ liệu (config, JSON response). Vấn đề: dict không có
shape — mypy không biết key nào tồn tại, value type gì. TypedDict giải quyết:
from typing import TypedDict, NotRequired
class User(TypedDict):
name: str
age: int
email: str | None
tags: NotRequired[list[str]] # Optional field — 3.11+
u: User = {"name": "An", "age": 22, "email": None} # OK, tags vắng được
# Truy cập như dict bình thường
print(u["name"]) # "An"
u["age"] = 23 # OK
u["unknown"] # Mypy: TypedDict "User" has no key "unknown"
# Mất type khi không annotate
data = json.loads(response) # type: dict[str, Any] — không phải User!
# Phải cast/validate:
user: User = cast(User, data)
TypedDict vs dataclass vs Pydantic — 3 lựa chọn cho "structured data":
| Tính chất | TypedDict | @dataclass | pydantic.BaseModel |
|---|---|---|---|
| Bản chất runtime | Dict bình thường | Class với __init__ | Class + runtime validation |
| Truy cập field | u["name"] | u.name | u.name |
| Runtime check? | Không | Không (chỉ type hint) | Có — validate input |
| Performance | Nhanh nhất (dict native) | Trung bình | Chậm hơn (validation overhead) |
| Serialize JSON | Trực tiếp json.dumps | Cần asdict() hoặc tự viết | Có .model_dump_json() |
| Nested validation | Mypy compile-time | Mypy compile-time | Runtime + descriptive errors |
| Use case | JSON shape, config dict | Domain object internal | API boundary, user input |
Khuyến nghị thực tế
TypedDict: response API đã JSON, không cần object — vd hàm trả dict trực tiếp ra
jsonify. Dataclass: domain object nội bộ (User, Order, Product) — có method,
có behavior. Pydantic: lớp ranh giới ứng dụng — input từ HTTP request, từ file YAML, từ
CLI args — nơi cần validate giá trị thật, không chỉ type.
11. Protocol — structural typing
Python có truyền thống duck typing: "nếu nó kêu quack thì nó là vịt". Trước Python 3.8, duck
typing thoải mái nhưng không kiểm tra. Protocol (PEP 544) đem static check cho duck
typing: định nghĩa shape mà object phải có, không cần kế thừa.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("○")
class Square:
def draw(self) -> None:
print("□")
# Cả Circle và Square KHÔNG kế thừa Drawable
# Nhưng đều "match" shape (có draw() -> None) — đủ rồi
def render(items: list[Drawable]) -> None:
for item in items:
item.draw()
render([Circle(), Square()]) # Mypy OK — duck typing được type check!
So sánh với ABC (Abstract Base Class) — pattern OOP truyền thống chương 5:
| Khía cạnh | Protocol (structural) | ABC (nominal) |
|---|---|---|
| Kiểm tra | Match shape (có method đúng signature) | Phải kế thừa tường minh |
| Linh hoạt | Cao — class nào có shape đều dùng được | Thấp — cần đổi class hierarchy |
| Áp dụng cho lib bên ngoài | Được — không cần sửa code lib | Khó — không thể inherit từ third-party |
| Runtime check | Có với @runtime_checkable | Có (qua isinstance) |
| Pythonic | Rất Pythonic (duck typing) | Hơi Java-style |
| Khi dùng | Mô tả "interface" cho function input | Cần share code chung (mixin) |
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
def safe_close(obj: object) -> None:
if isinstance(obj, Closeable): # Check runtime!
obj.close()
@runtime_checkable chỉ check method tồn tại
Không check signature có khớp không. Một class có method close() nhận 5 argument lạ vẫn pass
isinstance(obj, Closeable). Đây là trade-off vì Python không reflect được signature lúc
runtime trọn vẹn.
12. Generic function — PEP 695 (3.12+)
Generic là tham số type. Hàm hoạt động với bất kỳ type T nào — gặp ở list[T],
dict[K, V]. Tự viết function generic giờ rất ngắn nhờ PEP 695.
# Python 3.12+ — cú pháp [T] sau tên
def first[T](items: list[T]) -> T:
return items[0]
x: int = first([1, 2, 3]) # T = int
s: str = first(["a", "b"]) # T = str
# Nhiều type parameter
def swap[A, B](pair: tuple[A, B]) -> tuple[B, A]:
a, b = pair
return b, a
# Constraint: T phải là int hoặc float
def double[T: (int, float)](x: T) -> T:
return x * 2
# Bound: T phải là subtype của Comparable
def sort_items[T: SupportsRichComparison](items: list[T]) -> list[T]:
return sorted(items)
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# Constrained TypeVar
Num = TypeVar("Num", int, float)
def double(x: Num) -> Num:
return x * 2
# Bound TypeVar
TComp = TypeVar("TComp", bound=SupportsRichComparison)
Cú pháp TypeVar cũ vẫn dùng được mãi mãi. Nhưng nếu project ≥ Python 3.12, viết
def f[T] ngắn hơn và scope của T rõ ràng (chỉ trong function đó). Cú pháp cũ T là biến module-level —
nếu dùng ở 2 function khác nhau dễ hiểu lầm là "cùng T".
13. Generic class — PEP 695
# Python 3.12+ — cú pháp class Name[T]:
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()
s: Stack[int] = Stack()
s.push(1)
s.push("hello") # Mypy: incompatible type
# Generic class với nhiều type parameter
class Pair[A, B]:
def __init__(self, first: A, second: B) -> None:
self.first = first
self.second = second
p: Pair[str, int] = Pair("age", 22)
from 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) -> T: return self.items.pop()
14. Type narrowing — thu hẹp union
Khi biến có union (vd int | str hoặc T | None), không gọi method được ngay vì
method ở 2 type khác nhau. Cần narrow trước.
def format_id(x: int | str) -> str:
if isinstance(x, str):
return x.upper() # mypy: x đã được narrow về str
return str(x) # mypy: x là int ở đây
def greet(name: str | None) -> str:
if name is None:
return "Khách"
return name.upper() # name đã được narrow về str
# assert cũng narrow
def process(x: int | None) -> int:
assert x is not None
return x + 1 # x: int sau assert
from typing import TypeGuard
def is_str_list(x: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in x)
def process(items: list[object]) -> None:
if is_str_list(items):
for s in items:
print(s.upper()) # items đã được narrow về list[str]
# Python 3.13+ giới thiệu TypeIs — narrow chính xác hơn (cả 2 branch)
from typing import TypeIs # 3.13+
def is_int(x: object) -> TypeIs[int]:
return isinstance(x, int)
- TS:
typeof x === 'string'→ Python:isinstance(x, str). - TS:
x instanceof Foo→ Python:isinstance(x, Foo)(cùng tên). - TS: type predicate
x is T→ Python:TypeGuard[T]. - TS: discriminated union qua property literal → Python:
TypedDictvớiLiteralfield, kết hợpmatch.
Ý tưởng giống hệt, cú pháp khác. Cả 2 đều dựa trên control-flow analysis của tool.
15. Cài và chạy mypy
mypy là static type checker chính thức của Python (Guido + Jukka Lehtosalo phát triển). Cài
bằng pip, chạy trực tiếp lên file/folder.
# Cài
pip install mypy
# hoặc với uv (chương 1)
uv pip install mypy
# Chạy
mypy app.py # 1 file
mypy src/ # 1 folder
mypy --strict src/ # Strict mode — kiểm tra tối đa
# Output mẫu:
# app.py:12: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
# Found 1 error in 1 file (checked 5 source files)
Cấu hình qua pyproject.toml (chương 1) thay vì gõ flag mỗi lần:
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_any_unimported = true
no_implicit_optional = true
check_untyped_defs = true
show_error_codes = true
# Bỏ qua thư viện không có type stub
[[tool.mypy.overrides]]
module = ["untyped_lib.*"]
ignore_missing_imports = true
Cờ --strict bật một bộ cờ tất cả-trong-một: disallow_untyped_defs (cấm
function không annotate), disallow_any_explicit, warn_unused_ignores,
strict_optional, check_untyped_defs, warn_return_any,
no_implicit_optional, warn_redundant_casts, … Tổng cộng 10+ cờ.
- Project mới — bật
strict = truengay từ commit đầu. Không nợ nần. - Project cũ — bật
strictdần: chỉ apply cho module mới, dùngfilestrong config. - CI: chạy
mypymỗi PR, fail nếu có error mới (so với baseline). - IDE: cài plugin (Pylance VS Code / PyCharm tích hợp) để thấy lỗi khi gõ.
16. Pyright và Pylance
Pyright là type checker của Microsoft, viết bằng TypeScript (chạy trên Node). Nhanh hơn mypy đáng kể (5-10×) vì incremental check tốt. Pylance là extension VS Code đóng gói Pyright + auto-complete + refactor.
npm install -g pyright
pyright src/
pyright --strict src/
# Config qua pyrightconfig.json hoặc pyproject.toml
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"
reportMissingTypeStubs = true
| Khía cạnh | mypy | Pyright |
|---|---|---|
| Tác giả | Cộng đồng Python (Guido + Jukka) | Microsoft |
| Implementation | Python | TypeScript/Node |
| Tốc độ | Chậm hơn (full re-check) | Nhanh (incremental) |
| Tích hợp IDE | Plugin riêng | Pylance trong VS Code |
| Tương thích PEP | Reference cho PEP — implement chuẩn nhất | Đôi khi đi trước (vd PEP 695 sớm) |
| Strict mode | --strict | typeCheckingMode = "strict" |
| Lựa chọn | CI/CD chính thức | Dev IDE + check nhanh |
Nhiều team chạy Pyright trong IDE (Pylance VS Code) để dev nhanh và mypy trong CI để standardize. Không cần bắt buộc 1 lựa chọn — kết quả có thể khác đôi chút ở edge case, đa số giống nhau.
17. Anti-pattern: over-typing
Quy tắc kinh nghiệm: type hint nơi tạo giá trị contract, không phải mọi dòng.
def process(items: list[int]) -> int:
total: int = 0 # noise — mypy đã biết total: int
i: int = 0 # noise
n: int = len(items) # noise
while i < n:
x: int = items[i] # noise
squared: int = x * x # noise
total = total + squared
i = i + 1
return total
def process(items: list[int]) -> int:
total = 0
for x in items:
total += x * x
return total
Nơi nên annotate:
- Function signature: parameter và return type. Đây là contract — caller cần biết.
- Class attribute:
name: strtrên class body, hoặcself.x: int = 0. - Module-level constant:
MAX_RETRIES: Final = 3. - Biến local không có khởi tạo:
result: list[int] = [](mypy không suy được từ[]trống). - Biến local có type phức tạp mà tool suy nhầm — annotate để bắt rõ.
Nơi không cần annotate:
- Biến local có giá trị literal rõ ràng:
total = 0,name = "An". - Variable trong loop (
for x in items) — mypy biết x từ items type. - Comprehension intermediates.
- Return type nếu function trả về literal (
return 0) — mypy infer được. Nhưng nếu là public API, vẫn nên annotate return type rõ ràng.
Zen of Python: "Explicit is better than implicit" — nhưng cũng "Readability counts". Type
hint phải phục vụ người đọc. Nếu một dòng total = 0 đã rõ rành rành thì thêm : int
chỉ tốn không gian, không thêm thông tin.
So với TypeScript: TS cộng đồng đẩy mạnh inference (đừng annotate cái mà compiler đoán được). Python theo quan điểm tương tự — type hint là tài liệu + contract, không phải nghi lễ.
Bài tập
Bài 1 — Type hint đầy đủ một file, fix tới 0 lỗi mypy
Lấy một file .py bạn đã viết ở chương 1-8 (vd: module chương 8 dùng asyncio). Mục tiêu:
mypy --strict file.py trả về 0 error.
- Chạy
mypy --strict file.pylần đầu → đếm error. - Thêm annotation cho mỗi function signature (parameter + return).
- Annotate attribute class, constant module-level.
- Fix Optional: chỗ nào có thể return
Nonephải annotateT | None. - Loại bỏ implicit
Any— nếu lib không có stub, thêm vào configignore_missing_imports = true. - Mục tiêu cuối: 0 error.
Gợi ý chiến thuật
- Bắt đầu từ entry point (function
main) — đi vào trong dần. - Khi mypy phàn nàn về biến container rỗng (
items = []), annotate:items: list[int] = []. - Khi gặp
**kwargshoặc decorator phức tạp — tạm thời# type: ignore[code]ngắn gọn, fix sau. - Dùng
show_error_codes = trueđể biết error code (vd[arg-type]) — tra tài liệu mypy nhanh hơn.
Bài 2 — TypedDict cho response API
Định nghĩa TypedDict cho User và viết function nhận JSON response, validate shape, trả về
User typed.
- User có:
name: str,age: int,tags: list[str],email: str | None. - Function
parse_user(raw: dict) -> UserraiseValueErrornếu shape sai. - Test với 1 dict đúng, 1 dict thiếu key, 1 dict sai type.
Đáp án
from typing import TypedDict, cast
class User(TypedDict):
name: str
age: int
tags: list[str]
email: str | None
def parse_user(raw: dict[str, object]) -> User:
if not isinstance(raw.get("name"), str):
raise ValueError("name phải là str")
if not isinstance(raw.get("age"), int):
raise ValueError("age phải là int")
tags = raw.get("tags")
if not (isinstance(tags, list) and all(isinstance(t, str) for t in tags)):
raise ValueError("tags phải là list[str]")
email = raw.get("email")
if email is not None and not isinstance(email, str):
raise ValueError("email phải là str|None")
return cast(User, raw)
Trong production, đa số dev không tự viết — dùng pydantic để generate validation
tự động từ TypedDict/BaseModel. Bài này luyện hiểu cơ chế.
Bài 3 — Generic Repository[T]
Implement generic class Repository[T] có:
find_by_id(id: int) -> T | Nonesave(item: T) -> Noneall() -> list[T]
Lưu trữ trong memory dict. Test với Repository[User] và
Repository[Product] — mypy phải hiểu return type khác nhau.
Đáp án
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
@dataclass
class Product:
id: int
title: str
# Yêu cầu T có thuộc tính id: int → dùng Protocol
from typing import Protocol
class HasId(Protocol):
id: int
class Repository[T: HasId]:
def __init__(self) -> None:
self._store: dict[int, T] = {}
def find_by_id(self, id: int) -> T | None:
return self._store.get(id)
def save(self, item: T) -> None:
self._store[item.id] = item
def all(self) -> list[T]:
return list(self._store.values())
users: Repository[User] = Repository()
users.save(User(1, "An"))
u = users.find_by_id(1) # u: User | None
products: Repository[Product] = Repository()
products.save(Product(1, "Bút"))
Bài 4 — Protocol Comparable và min_of
Viết Protocol Comparable có __lt__. Function
min_of(items: list[T]) -> T với T constraint là Comparable. Test với int,
str, và một class custom có __lt__.
Đáp án
from typing import Protocol
class Comparable(Protocol):
def __lt__(self, other: object, /) -> bool: ...
def min_of[T: Comparable](items: list[T]) -> T:
if not items:
raise ValueError("list rỗng")
best = items[0]
for x in items[1:]:
if x < best:
best = x
return best
print(min_of([3, 1, 2])) # 1
print(min_of(["banh", "an", "chao"])) # "an"
class Score:
def __init__(self, n: int) -> None: self.n = n
def __lt__(self, other: object, /) -> bool:
return isinstance(other, Score) and self.n < other.n
def __repr__(self) -> str: return f"Score({self.n})"
print(min_of([Score(5), Score(2), Score(8)])) # Score(2)
Bài 5 — TypeGuard is_str_list
Viết def is_str_list(x: list[object]) -> TypeGuard[list[str]] — check tất cả phần tử
là str. Dùng nó để narrow trong function khác.
Đáp án
from typing import TypeGuard
def is_str_list(x: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in x)
def join_uppercase(items: list[object]) -> str:
if is_str_list(items):
return ", ".join(s.upper() for s in items)
raise TypeError("không phải list[str]")
print(join_uppercase(["a", "b"])) # "A, B"
print(join_uppercase(["a", 1])) # TypeError
Lưu ý: TypeGuard[T] narrow trong branch True nhưng không narrow
ngược lại trong branch False. Nếu cần narrow cả 2 branch, dùng
TypeIs[T] (Python 3.13+).
Quiz
Type hints có ảnh hưởng runtime không?
Xem đáp án
Không. Python interpreter thấy annotation (lưu trong
__annotations__) nhưng không kiểm tra. Gán x: int = "hello" chạy bình
thường, không AttributeError.
Trừ khi: bạn chủ động dùng thư viện runtime validate như pydantic (cực
phổ biến cho API), beartype, typeguard. Các thư viện này đọc annotation và
tự generate validation code.
Ngoài ra, runtime vẫn truy cập được annotation qua typing.get_type_hints() — hữu ích khi
viết framework tự sinh code (vd FastAPI tự đọc signature để parse request body).
Khác nhau giữa def f(x: list) và def f(x: list[int]) với mypy?
Xem đáp án
list không có type parameter — mypy hiểu là list[Any]. Mọi list đều pass.
list[int] chặt hơn: chỉ chấp nhận list mà mỗi phần tử là int.
Truy cập x[0]: ở list trả Any (mất type), ở list[int]
trả int (giữ type).
Với strict = true hoặc disallow_any_generics = true, mypy sẽ cấm
viết list không có parameter — buộc phải list[int] hoặc list[Any]
tường minh.
Optional[int] và int | None khác gì?
Xem đáp án
Hoàn toàn tương đương. Optional[T] chỉ là alias cho Union[T, None]
— mà Union[T, None] giờ viết được là T | None (PEP 604, Python 3.10+).
Khuyến nghị dùng T | None: ngắn hơn, không cần import, đọc tự nhiên hơn. Optional
là tên dễ gây hiểu lầm: nó không có nghĩa "parameter optional" (có default), mà chỉ là "có thể
là None".
Any và object khác gì?
Xem đáp án
Any tắt type check — gọi mọi method, gán đi đâu cũng được, không cần
narrow. Backdoor.
object là supertype của mọi type — mọi thứ đều assign được vào
object. Nhưng từ object chỉ gọi được method của object (
__str__, __repr__, __eq__, __hash__). Muốn dùng
method khác phải narrow bằng isinstance.
object là pattern thay unknown của TS trong Python: an toàn nhưng vẫn phải
xử lý. Quy tắc: thấy Any → thử thay bằng object hoặc Protocol cụ thể.
Protocol khác ABC ở điểm nào?
Xem đáp án
Protocol = structural typing: class match nếu có shape đúng (method đúng tên, signature đúng). Không cần kế thừa.
ABC = nominal typing: class phải kế thừa tường minh từ ABC mới được coi là subtype.
Protocol Pythonic hơn vì giữ tinh thần duck typing: "nếu nó kêu quack thì nó là vịt" — không quan tâm cha mẹ là ai. ABC giống Java/C# truyền thống: class hierarchy quan trọng.
Quy tắc thực dụng: Protocol cho interface "input của function" (linh hoạt cao). ABC khi cần share code chung qua kế thừa (mixin pattern).
Cú pháp TypeVar cũ và def f[T] PEP 695 khác gì?
Xem đáp án
Hoàn toàn tương đương về mặt ngữ nghĩa. PEP 695 (Python 3.12+) thêm cú pháp ngắn hơn.
- Cũ:
T = TypeVar("T")module-level, dùng trong nhiều function — nếu T xuất hiện 2 chỗ độc lập dễ nhầm "cùng T". - Mới:
def f[T](...)— T scope local trong function, không lẫn lộn. - Class:
class Stack[T]thayclass Stack(Generic[T])— không cần kế thừa Generic.
Khuyến nghị: project mới (Python 3.12+) dùng PEP 695 ngay. Code cũ duy trì TypeVar không cần migrate gấp — cả 2 cú pháp được hỗ trợ song song lâu dài.
Mypy --strict bật những flag chính nào?
Xem đáp án
--strict là alias bật 10+ cờ cùng lúc:
--disallow-untyped-defs: cấm function không annotate.--disallow-incomplete-defs: cấm annotate nửa vời (chỉ parameter, thiếu return).--disallow-any-explicit(gián tiếp): hạn chế dùngAny.--no-implicit-optional:def f(x: int = None)không tự thànhint | None.--warn-return-any: cảnh báo khi function trảAnydù annotate return type khác.--warn-unused-ignores: cảnh báo# type: ignorekhông cần thiết.--check-untyped-defs: vẫn kiểm tra body của function chưa annotate.--strict-equality: cấm so sánh==giữa 2 type không overlap.--warn-redundant-casts,--warn-unreachable, …
Bật strict = hành xử như TypeScript strict. Khuyến nghị mọi project mới.
Tổng kết
Sau chương 9, bạn nên đã master:
- Type hints là compile-time metadata, runtime bỏ qua — trừ khi dùng pydantic/beartype.
- Container generic:
list[T],dict[K, V],tuple[T, ...]— dùng built-in từ 3.9+. - Optional và Union:
T | NonethayOptional[T],A | BthayUnion[A, B](3.10+). - 4 type đặc biệt:
Any(tắt check, tránh),object(an toàn, phải narrow),Never(no return),Literal(giá trị cụ thể). Final: chặn reassign tên biến (compile-time only).- TypeAlias: đặt tên type. PEP 695 dùng keyword
type. - TypedDict: dict có shape. Khác dataclass (object) và pydantic (runtime validate).
- Protocol: structural typing — duck typing có type check. Khác ABC ở chỗ không cần inherit.
- Generic PEP 695:
def f[T],class Stack[T]— ngắn gọn, scope rõ. - Narrowing:
isinstance,is None,assert,TypeGuard. - Mypy + Pyright: cài, config qua
pyproject.toml, chạy--strict. - Tránh over-typing: annotate function signature + attribute, không annotate biến local literal.
Kết nối
- Chương 5 (OOP in Python) — Protocol vs ABC: nhánh OOP đối chiếu typing.
- Chương 6 (Iterators & Generators) —
Iterator[T],Generator[Yield, Send, Return]dùng generic. - Chương 7 (Decorators) — typing decorator nâng cao với
ParamSpec,Concatenate. - Chương 8 (Async) —
Coroutine[Any, Any, T],Awaitable[T],AsyncIterator[T]. - Chương 10 (Testing & Tooling) — chạy mypy + ruff trong CI,
pytestvới type hint cho fixture. - JavaScript Chương 9 (TypeScript Foundations) — đối chiếu:
any↔Any,unknown↔object,never↔Never, type predicate↔TypeGuard. - JavaScript Chương 10 (Generics & Utility Types) — generic function/class TS vs PEP 695.
- JavaScript Chương 11 (Advanced Types) — mapped, conditional — Python ít trực tiếp tương ứng, một số có ở
typing.Protocolnâng cao. - Dart Chương 2 — sound null safety: runtime check, khác hẳn Python
T | Nonechỉ compile-time.