← Lập trình Python nâng cao

Bài 5 · Nâng cao · 22 phút· Cập nhật 11/06/2026

Type hints & mypy

Biên soạn bởi Nguyễn Anh Tuấn

Type hints & kiểm kiểu tĩnh với mypy: typing, generics, Protocol, TypedDict - “gõ kiểu” Python cho dự án lớn, bắt lỗi trước khi chạy.

Python là ngôn ngữ kiểu động. Type hint thêm chú thích kiểu vào chữ ký; quan trọng: Python bỏ qua chúng khi chạy. Chúng dành cho người đọc, editor, và mypy:

hint.py - chạy y hệt bản không hint

def cong(a: int, b: int) -> int:   # a, b la int; tra ve int
    return a + b

ten: str = "An"
diem: list[int] = [8, 9, 10]
print(cong(2, 3))

Kết quả khi chạy

5
  • Hint là chú thích: cong(a: int) chạy như cong(a) - không ép kiểu lúc chạy.
  • Lợi: tài liệu sống, editor gợi ý đúng, mypy bắt lỗi kiểu trước khi chạy.
  • Muốn KIỂM lúc chạy (dữ liệu từ ngoài) cần thư viện như pydantic.

mypy đọc code + hint và báo lỗi mà không chạy. Bấm thử từng lời gọi xem mypy chấp nhận hay từ chối:

def cong(a: int, b: int) -> int: ...

Bấm một dòng - xem mypy CHẤP NHẬN hay BÁO LỖI (trước khi chạy):

cong(2, 3) mypy: OK

int khớp int → hợp lệ.

★ Type hint KHÔNG đổi cách chạy - Python vẫn chạy như cũ. Nhưng mypy đọc hint và bắt lỗi kiểu TRƯỚC khi chạy, như một “lưới an toàn” cho dự án lớn.

chạy mypy + soi kiểu bằng reveal_type

# app.py
def cong(a: int, b: int) -> int:
    return a + b

reveal_type(cong(2, 3))     # cong cu phap cua mypy de SOI kieu

# Chay:
#   uv run --with mypy mypy app.py
# mypy in:
#   app.py: note: Revealed type is "builtins.int"
  • mypy = kiểm kiểu TĨNH: bắt gọi sai kiểu, quên None… trước khi chạy.
  • reveal_type(x) → chạy mypy → in KIỂU mypy suy ra cho x (gỡ lỗi kiểu rất tiện).
  • bool là int (tháp số bool <: int <: float); float KHÔNG tự thu hẹp về int.

modern.py

def tong(ds: list[int]) -> int:
    return sum(ds)

def diem(d: dict[str, int], ten: str) -> int | None:
    return d.get(ten)                 # int neu co, None neu khong

def dau_tien[T](ds: list[T]) -> T:    # generic PEP 695 (3.12+)
    return ds[0]

type Vector = list[float]             # bi danh kieu

print(tong([1, 2, 3]), diem({"An": 9}, "Binh"), dau_tien(["x", "y"]))

Kết quả khi chạy

6 None x
  • Dùng list[int], dict[str, int], tuple[int, str] - kiểu dựng sẵn làm generic.
  • X | None = Optional (3.10+): “kiểu X hoặc vắng” → buộc xử lý None tử tế.
  • def ham[T](...) / class Lop[T] / type Alias = ... : generic & alias gọn (3.12+).

Điểm mạnh nhất của hệ kiểu Python là structural typing qua Protocol: object KHỚP nếu có đủ HÌNH DẠNG (phương thức), KHÔNG cần kế thừa:

protocol.py

from typing import Protocol, runtime_checkable

@runtime_checkable
class Keu(Protocol):
    def keu(self) -> str: ...        # ai co .keu() deu "la Keu"

class Cho:
    def keu(self): return "Gau"
class Xe:
    def chay(self): return "vroom"

def cho_keu(x: Keu) -> str:          # nhan BAT KY thu gi co .keu()
    return x.keu()

print(cho_keu(Cho()))                # OK
print(isinstance(Cho(), Keu), isinstance(Xe(), Keu))   # runtime_checkable
# cho_keu(Xe())  -> mypy: incompatible type "Xe"; expected "Keu"

Kết quả khi chạy

Gau
True False
  • Protocol = gõ kiểu theo HÌNH DẠNG: có đủ phương thức là khớp, không cần kế thừa.
  • mypy bắt lỗi khi truyền object thiếu phương thức (vd Xe không có .keu()).
  • @runtime_checkable + isinstance() kiểm được lúc chạy (theo sự hiện diện phương thức).

Hai công cụ làm hint khớp dữ liệu thật (JSON, tham số chế độ…):

typeddict_literal.py

from typing import TypedDict, Literal

class NguoiDung(TypedDict):           # dict CO kieu cho tung khoa
    ten: str
    tuoi: int

u: NguoiDung = {"ten": "An", "tuoi": 20}   # thieu khoa / sai kieu -> mypy bao

def set_che_do(m: Literal["doc", "ghi"]) -> None:   # chi nhan 2 gia tri
    ...

set_che_do("doc")                     # OK
# set_che_do("xoa")  -> mypy: incompatible type Literal['xoa']; expected Literal['doc','ghi']
  • TypedDict: kiểu cho dict khoá cố định (rất hợp dữ liệu JSON) - vẫn là dict thường lúc chạy.
  • Literal["doc","ghi"]: giới hạn giá trị vào tập cụ thể → bắt giá trị lạ.
  • Kết hợp với Protocol/Optional → hệ kiểu mô tả đúng ý định, mypy bắt được nhiều bug thật.

Dù Python bỏ qua hint khi chạy logic, hint vẫn được LƯU và đọc được - đó là cách dataclass/pydantic/FastAPI hoạt động:

runtime_hints.py

from typing import get_type_hints

def cong(a: int, b: int) -> int:
    return a + b

print(cong.__annotations__)          # {'a': int, 'b': int, 'return': int}
print(get_type_hints(cong))          # tuong tu, da giai ten kieu chuan hon

Kết quả khi chạy

{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
  • Hint lưu ở __annotations__; get_type_hints() giải tên kiểu chuẩn hơn.
  • dataclass/pydantic/FastAPI ĐỌC hint lúc chạy để sinh __init__, validate, tạo tài liệu API.
  • Gradual typing: thêm hint tới đâu mypy kiểm tới đó; Any = lỏng (tắt kiểm), object = chặt.

Tiếp theo

Type hint bắt lỗi kiểu trước khi chạy. Nhưng đúng kiểu chưa chắc đúng hành vi - đó là việc của kiểm thử. Bài kế tiếp: unit test với pytest.

Câu hỏi thường gặp

Không. Python BỎ QUA type hint khi chạy (chúng chỉ là chú thích). def cong(a: int, b: int) -> int chạy y hệt def cong(a, b). Hint là để CON NGƯỜI, EDITOR và công cụ như mypy đọc. Muốn ép/kiểm lúc chạy thì phải tự viết kiểm tra hoặc dùng thư viện (vd pydantic - nó ĐỌC hint lúc chạy).

Chèn reveal_type(x) vào code rồi chạy mypy - mypy in ra KIỂU NÓ SUY RA cho x (vd revealed type is "builtins.int"). Cực hữu ích để gỡ lỗi khi không chắc mypy hiểu kiểu thế nào. (Python 3.11+ cũng có typing.reveal_type chạy được, nhưng lúc chạy nó in KIỂU RUNTIME ra stderr - công dụng chính vẫn là với mypy.)

Protocol cho phép gõ kiểu theo HÌNH DẠNG (structural / "duck typing có kiểu"): một object KHỚP Protocol nếu nó có đủ phương thức/thuộc tính cần - KHÔNG cần kế thừa. Vd Protocol Keu yêu cầu .keu(); mọi lớp có .keu() đều "là Keu" với mypy, dù không liên quan gì nhau. Thêm @runtime_checkable thì isinstance() cũng kiểm được lúc chạy.

TypedDict: mô tả KIỂU cho một dict có khoá cố định (vd dữ liệu JSON {"ten": str, "tuoi": int}) → mypy bắt lỗi khoá sai/kiểu sai mà vẫn là dict thường lúc chạy. Literal: giới hạn giá trị vào một tập cụ thể, vd Literal["doc", "ghi"] → truyền "xoa" là mypy báo lỗi. Cả hai làm hint sát thực tế hơn nhiều.

Hint lưu ở ham.__annotations__ (hoặc typing.get_type_hints(obj) để giải tên kiểu cho chuẩn). Đây là cách @dataclass, pydantic, FastAPI… ĐỌC hint lúc chạy để tự sinh __init__, validate dữ liệu, hay tạo tài liệu API. Hint "vô hại lúc chạy" nhưng các thư viện này khai thác nó.

Any = "tắt kiểm kiểu" ở chỗ đó: gán đi gán lại, gọi gì cũng được, mypy không phàn nàn (dễ dãi, rủi ro). object = cha của MỌI kiểu: nhận được mọi giá trị, nhưng bạn chỉ làm được những gì object hỗ trợ (an toàn, phải narrow trước khi dùng). Lưu ý: unknown là của TypeScript - Python KHÔNG có; cặp đúng là Any (lỏng) vs object (chặt).

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Type hint ảnh hưởng thế nào tới cách Python chạy chương trình?

  1. 1

    mypy bắt lỗi + reveal_type

    Viết cong(a:int,b:int)->int; gọi cong("2",3); thêm reveal_type(cong(2,3)). Chạy uv run --with mypy mypy file.py.

    Hoàn thành khi: mypy báo incompatible type "str"; expected "int" [arg-type], và in revealed type là int.

  2. 2

    Optional

    Viết tim(ds, x) -> int | None; dùng mypy kiểm nơi gọi có xử lý None trước khi dùng như int chưa.

    Hoàn thành khi: Kiểu trả int | None; mypy nhắc nếu dùng kết quả như int mà chưa loại None.

  3. 3

    Protocol (duck typing có kiểu)

    Định nghĩa Protocol Keu (có .keu()->str). Viết hàm nhận Keu; gọi với một lớp có .keu() và một lớp KHÔNG có; chạy mypy.

    Hoàn thành khi: Lớp có .keu() qua được; lớp không có bị mypy báo incompatible type - dù không kế thừa gì.

  4. 4

    TypedDict & Literal

    Khai TypedDict NguoiDung{ten:str,tuoi:int} và hàm set_che_do(m: Literal["doc","ghi"]). Thử truyền sai khoá/giá trị; chạy mypy.

    Hoàn thành khi: mypy bắt khoá/kiểu sai trong TypedDict, và giá trị ngoài Literal (vd "xoa").

  5. 5

    Hint lúc chạy

    In ham.__annotations__typing.get_type_hints(ham) cho một hàm có hint.

    Hoàn thành khi: Thấy dict {tên: kiểu}; bạn giải thích vì sao dataclass/pydantic đọc được hint này.