# Type hint và pyright: đọc nhanh codebase của khách, bắt lỗi None trước khi chạy

> Bạn chưa sửa dòng nào trong repo của khách, vậy mà pyright đã có thể cho biết hàm nào sẽ trả về None khi dữ liệu thật thiếu một giá trị.

Bản gốc: https://fdetimes.net/vi/bach-khoa/python-type-hints-pyright-doc-code-khach/

Ngày thứ hai ở chỗ khách, bạn được cấp quyền vào một repo Python đã vài năm tuổi. Người viết phần tính phí đã nghỉ việc, còn hàm nào cũng chỉ có tên tham số, không có lấy một dòng chú thích. Thứ Sáu này bạn phải nối pipeline của mình vào đúng đoạn code đó.

Thường thì người ta đọc từng file, rải `print` khắp nơi rồi chạy thử với dữ liệu mẫu. Cách này chậm, và nó bỏ sót đúng loại lỗi hay làm sập deployment thật: một hàm luôn trả về giá trị khi chạy với dữ liệu mẫu, nhưng lại trả về `None` khi dữ liệu của khách thiếu một dòng.

Type hint đi cùng một type checker như pyright giải quyết được cả hai việc. Bạn đọc code lạ nhanh hơn vì biết mỗi hàm nhận gì và trả gì, và bạn bắt được lỗi trước khi chạy. FDE gần như lúc nào cũng làm việc trên code mình không viết, nên đây là kỹ năng đáng đầu tư.

## Python không kiểm tra kiểu thì ai kiểm tra?

Hiểu sai chỗ này thì mọi việc sau đều lệch, nên cần nói rõ trước. Tài liệu chính thức của module `typing` ghi rằng lúc chạy, Python không ép kiểu cho annotation của hàm hay biến. Annotation là để công cụ bên thứ ba đọc: type checker, IDE, linter.

Chuyện này được định sẵn từ đầu. PEP 3107 đưa vào cú pháp annotation cho hàm nhưng cố ý không quy định nó mang nghĩa gì. Phải đến PEP 484, annotation mới được dùng để chỉ kiểu. Chính PEP 484 cũng khẳng định Python sẽ vẫn là ngôn ngữ kiểu động, và các tác giả không muốn ép ai phải viết type hint.

Với bạn, điều này có hai hệ quả. Viết `def f(x: int)` không ngăn được ai truyền vào một chuỗi. Ngược lại, thêm hint vào code của khách cũng không làm code chạy khác đi, nên việc này gần như không có rủi ro. Hint chỉ thật sự có ích khi có một công cụ đọc chúng.

## Vì sao pyright hợp với code không có hint

Pyright là một static type checker. Bạn có thể cài nó làm language server trong editor hoặc dùng bản command-line. Theo tài liệu của pyright, bạn không cần chú thích hết mới dùng được: thông tin kiểu càng nhiều thì nó càng giúp được nhiều.

Với code kế thừa, điểm quan trọng là pyright không chờ bạn viết hint. Theo tài liệu, ở cấu hình mặc định nó kiểm tra mọi đoạn code, dù có annotation hay không.

Nó suy ra kiểu của biến từ giá trị được gán vào. Kiểu trả về của hàm được coi là union của kiểu ở mọi câu lệnh `return`. Chỗ nào không suy ra được thì pyright coi là `Unknown`, một dạng của `Any`.

Mypy mặc định làm khác. Cũng theo tài liệu so sánh của pyright, mypy bỏ qua mọi hàm và method không có type annotation.

Đây là một cái bẫy. Trên repo không có hint, mypy báo sạch không có nghĩa là code ổn, chỉ có nghĩa là nó chưa đọc phần lớn code.

Tài liệu của pyright còn cho biết trên codebase lớn, pyright nhanh hơn mypy 3 đến 5 lần là chuyện thường gặp.

## Một lỗi None, đi từ đầu đến cuối

Thử hình dung đoạn code tính phí theo vùng dưới đây. Code chạy tốt trên máy người viết, vì dữ liệu thử nghiệm lúc nào cũng có vùng EU.

```python
def load_rate(rows, region):
    for row in rows:
        if row["region"] == region:
            return float(row["rate"])
    return None

def price_with_tax(rows, region, price):
    rate = load_rate(rows, region)
    return rate * price
```

Không dòng nào có annotation. Bạn chạy `pyright` trên thư mục này mà không sửa gì. `rows` không có kiểu nên pyright coi `row` là Unknown. Nhưng chỉ hai câu lệnh `return` đã đủ để nó suy ra `load_rate` trả về `float | None`.

Hệ quả nằm ở dòng cuối. Ở đó `rate` mang kiểu `float | None`, và nhân `None` với một con số thì chương trình sẽ dừng. Vì thông tin kiểu này đã có sẵn trước khi chạy, dòng đó là chỗ đầu tiên nên soi khi đọc kết quả của pyright. Nó trỏ đúng tới tình huống làm hỏng hóa đơn: khách có một vùng chưa có trong bảng giá.

Nếu dùng mypy với cấu hình mặc định, cả hai hàm đều bị bỏ qua vì không có annotation.

Cách sửa gồm hai bước. Bước đầu là viết rõ ý định bằng type hint. Cheat sheet của mypy hướng dẫn viết `X | None` cho giá trị có thể là `None`, còn `Optional[X]` cũng mang đúng nghĩa đó:

```python
def load_rate(rows: list[dict[str, str]], region: str) -> float | None:
    for row in rows:
        if row["region"] == region:
            return float(row["rate"])
    return None

def price_with_tax(rows: list[dict[str, str]], region: str, price: float) -> float:
    rate = load_rate(rows, region)
    if rate is None:
        raise ValueError(f"Chưa có thuế suất cho vùng {region}")
    return rate * price
```

Bước thứ hai là phép kiểm tra `if rate is None`. Sau dòng đó, type checker thu hẹp kiểu của `rate` thành `float`. Code vẫn ngắn nhưng giờ đã nói rõ phải làm gì khi thiếu dữ liệu. Đây cũng là câu nên mang đi hỏi khách: vùng chưa có thuế suất thì báo lỗi hay dùng một mức mặc định?

**Điểm mấu chốt:** Type checker không hiểu code thay bạn, nó chỉ cho bạn biết phải hỏi khách câu gì.

## Làm trên repo của khách theo thứ tự nào?

Bước một là chạy pyright với cấu hình mặc định, chưa sửa gì. Theo tài liệu, `typeCheckingMode` của pyright có bốn mức là off, basic, standard và strict, mặc định là standard. Lần chạy đầu tiên cho bạn một tấm bản đồ: file nào nhiều lỗi, hàm nào có thể trả về `None`, chỗ nào toàn Unknown.

Bước hai là thêm hint ở chỗ dữ liệu đi vào trước: những hàm đọc file, gọi API, truy vấn database, hoặc nhận đầu vào từ hệ thống của khách.

Lý do nằm ở cheat sheet của mypy: tham số không có kiểu bị coi là `Any`, và với `Any` thì công cụ không bắt được lỗi nào.

Bước ba là ghi cấu hình vào `pyrightconfig.json` trong nhánh của bạn, rồi đưa lệnh `pyright` vào CI nếu khách đồng ý. Phần code cũ để ở mức standard. Module mới do bạn viết thì có thể đặt strict, vì không có gì cũ phải dọn.

## Những sai lầm hay gặp

Sai lầm đầu tiên là tưởng type hint bảo vệ code lúc chạy. Không phải vậy. Nếu dữ liệu từ API của khách có thể sai kiểu, bạn vẫn phải tự kiểm tra ngay chỗ nhận dữ liệu.

Sai lầm thứ hai là tin vào kết quả sạch của mypy trên một repo chưa có annotation, như trong ví dụ trên. Sai lầm thứ ba là rải `Any` khắp nơi cho hết lỗi đỏ. Số lỗi trông đẹp hơn, nhưng công cụ cũng chẳng còn kiểm tra được gì.

Sai lầm cuối cùng là bật strict cho cả repo cũ ngay từ ngày đầu. Bạn sẽ nhận một danh sách lỗi dài, khách sẽ thấy một pull request sửa hàng trăm file, còn những lỗi `None` thật sự quan trọng thì chìm giữa đống đó.

Muốn đưa kỹ năng này vào CV, hãy ghi lại số lỗi pyright báo lúc mới tiếp quản repo và những lỗi bạn đã chặn được trước khi gặp dữ liệu thật. Một dòng như vậy cho nhà tuyển dụng thấy bạn làm quen với codebase lạ nhanh đến đâu, điều mà chữ "Python" chung chung không làm được.

Lần sau được giao một repo không có tài liệu, việc đầu tiên nên làm là chạy pyright, trước cả khi viết câu `print` nào. Danh sách lỗi đầu tiên đó thường cho bạn biết khá rõ dữ liệu của khách có thể làm code hỏng ở đâu.

**Thử ngay tuần này:**

- Cài pyright bản command-line rồi chạy trên một repo Python bạn đang tiếp quản, giữ nguyên code và cấu hình mặc định. Ghi lại các lỗi liên quan đến None.
- Chọn một hàm đọc dữ liệu từ bên ngoài, thêm type hint với X | None, xử lý trường hợp None ở chỗ gọi hàm, rồi chạy lại pyright cho đến khi hết lỗi.

## Nguồn

- [typing — Support for type hints — Python 3.15.1 documentation](https://docs.python.org/3/library/typing.html)

- [PEP 484 – Type Hints](https://peps.python.org/pep-0484/)

- [Pyright docs: Getting Started](https://raw.githubusercontent.com/microsoft/pyright/main/docs/getting-started.md)

- [Pyright docs: Configuration](https://raw.githubusercontent.com/microsoft/pyright/main/docs/configuration.md)

- [Pyright docs: Type Inference](https://raw.githubusercontent.com/microsoft/pyright/main/docs/type-inference.md)

- [Pyright docs: Mypy Comparison](https://raw.githubusercontent.com/microsoft/pyright/main/docs/mypy-comparison.md)

- [Type hints cheat sheet](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html)
