Chương 09 · Typing & Static Analysis

Typing & Static Analysis — đưa kiểm tra kiểu vào Python

Python là ngôn ngữ dynamic typing: một biến có thể đổi từ int sang str bất cứ lúc nào và runtime không phàn nàn. Từ Python 3.5 (PEP 484), type hints được thêm vào như một lớp tự nguyện: viết x: int = 5def f(x: int) -> str để mô tả contract. Runtime vẫn bỏ qua hoàn toàn — nhưng công cụ mypy, Pyright đọc, kiểm tra trước khi chạy và bắt bug mà unit test sẽ bỏ sót. Đây là phép tách "viết JavaScript với TypeScript" mang sang Python: cùng triết lý, khác cú pháp, khác trải nghiệm.

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1-8
🎯 Mục tiêu chương
  • 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 @dataclasspydantic.
  • 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ết Pyright khá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:

Cú pháp cơ bản
# 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
🧠 Type hints là metadata, không phải runtime check

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 systemTool checkBắt buộc?
TypeScriptCompile-time, strip khi buildtsc chính thứcCó (file .ts phải pass)
PythonAnnotation, runtime ignoremypy, Pyright bên thứ 3Không (tự nguyện)
DartSound static, runtime checkCompiler tích hợpCó (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].

Generic container — cú pháp hiện đại (3.9+)
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]}
🔥 Trước Python 3.9 — phải import từ 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]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.

2 cú pháp tương đương
# 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
💡 Khuyên dùng 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]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.

Union nhiều type
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.

Any disable check
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.

💡 Khi nào Any là chấp nhận được?
  • Dữ liệu thật sự không biết shape — vd **kwargs truyề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.

Never — function không return
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
🧠 Exhaustiveness check với Never
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.

Literal type
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
💡 Literal vs Enum

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.

Final
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 ≠ immutable

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ó.

TypeAlias — cũ và mới
# 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:

TypedDict — dict có cấu trúc
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ấtTypedDict@dataclasspydantic.BaseModel
Bản chất runtimeDict bình thườngClass với __init__Class + runtime validation
Truy cập fieldu["name"]u.nameu.name
Runtime check?KhôngKhông (chỉ type hint) — validate input
PerformanceNhanh nhất (dict native)Trung bìnhChậm hơn (validation overhead)
Serialize JSONTrực tiếp json.dumpsCần asdict() hoặc tự viết.model_dump_json()
Nested validationMypy compile-timeMypy compile-timeRuntime + descriptive errors
Use caseJSON shape, config dictDomain object internalAPI 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.

Protocol — structural typing
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ạnhProtocol (structural)ABC (nominal)
Kiểm traMatch shape (có method đúng signature)Phải kế thừa tường minh
Linh hoạtCao — class nào có shape đều dùng đượcThấp — cần đổi class hierarchy
Áp dụng cho lib bên ngoàiĐược — không cần sửa code libKhó — không thể inherit từ third-party
Runtime checkCó với @runtime_checkableCó (qua isinstance)
PythonicRất Pythonic (duck typing)Hơi Java-style
Khi dùngMô tả "interface" cho function inputCần share code chung (mixin)
Protocol với @runtime_checkable
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.

Generic function — cú pháp mới (3.12+)
# 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)
Trước PEP 695 — cú pháp TypeVar (cũ, verbose)
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

Generic class — cú pháp mới
# 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)
Trước PEP 695 — kế thừa Generic[T]
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.

isinstance narrowing
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
is None narrowing
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
TypeGuard — custom predicate
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)
🧠 So sánh narrowing Python vs TypeScript
  • 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: TypedDict với Literal field, kết hợp match.

Ý 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.

Setup mypy
# 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:

pyproject.toml — config mypy
[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ờ.

💡 Workflow điển hình
  1. Project mới — bật strict = true ngay từ commit đầu. Không nợ nần.
  2. Project cũ — bật strict dần: chỉ apply cho module mới, dùng files trong config.
  3. CI: chạy mypy mỗi PR, fail nếu có error mới (so với baseline).
  4. IDE: cài plugin (Pylance VS Code / PyCharm tích hợp) để thấy lỗi khi gõ.

16. PyrightPylance

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.

Pyright CLI
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ạnhmypyPyright
Tác giảCộng đồng Python (Guido + Jukka)Microsoft
ImplementationPythonTypeScript/Node
Tốc độChậm hơn (full re-check)Nhanh (incremental)
Tích hợp IDEPlugin riêngPylance trong VS Code
Tương thích PEPReference cho PEP — implement chuẩn nhấtĐôi khi đi trước (vd PEP 695 sớm)
Strict mode--stricttypeCheckingMode = "strict"
Lựa chọnCI/CD chính thứcDev IDE + check nhanh
💡 Chạy cả 2?

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.

❌ Over-typing
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
✅ Type hint vừa đủ
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: str trên class body, hoặc self.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.
🧠 Triết lý Python: explicit but not pedantic

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.

  1. Chạy mypy --strict file.py lần đầu → đếm error.
  2. Thêm annotation cho mỗi function signature (parameter + return).
  3. Annotate attribute class, constant module-level.
  4. Fix Optional: chỗ nào có thể return None phải annotate T | None.
  5. Loại bỏ implicit Any — nếu lib không có stub, thêm vào config ignore_missing_imports = true.
  6. 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 **kwargs hoặ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) -> User raise ValueError nế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 | None
  • save(item: T) -> None
  • all() -> list[T]

Lưu trữ trong memory dict. Test với Repository[User]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 Comparablemin_of

Viết Protocol Comparable__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

Q1

Type hints có ảnh hưởng runtime không?

Xem đáp án
✓ Đá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).

Q2

Khác nhau giữa def f(x: list)def f(x: list[int]) với mypy?

Xem đáp án
✓ Đá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.

Q3

Optional[int]int | None khác gì?

Xem đáp án
✓ Đá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".

Q4

Anyobject khác gì?

Xem đáp án
✓ Đá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.

objectsupertype 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ể.

Q5

Protocol khác ABC ở điểm nào?

Xem đáp án
✓ Đá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).

Q6

Cú pháp TypeVar cũ và def f[T] PEP 695 khác gì?

Xem đáp án
✓ Đá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] thay class 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.

Q7

Mypy --strict bật những flag chính nào?

Xem đáp án
✓ Đá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ùng Any.
  • --no-implicit-optional: def f(x: int = None) không tự thành int | None.
  • --warn-return-any: cảnh báo khi function trả Any dù annotate return type khác.
  • --warn-unused-ignores: cảnh báo # type: ignore khô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 | None thay Optional[T], A | B thay Union[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, pytest với type hint cho fixture.
  • JavaScript Chương 9 (TypeScript Foundations) — đối chiếu: anyAny, unknownobject, neverNever, 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.Protocol nâng cao.
  • Dart Chương 2 — sound null safety: runtime check, khác hẳn Python T | None chỉ compile-time.