Có cần chạy 5 trình kiểm tra kiểu dữ liệu Python cùng lúc không?
Bài viết thảo luận về áp lực khi phải duy trì nhiều trình kiểm tra kiểu dữ liệu Python như Mypy, Pyrefly và Pyright. Tác giả đề xuất thay vì làm lộn xộn mã nguồn bằng các chú thích bỏ qua, nhà phát triển nên chạy các công cụ này trên bộ kiểm thử để đảm bảo API công khai hoạt động tốt cho người dùng.

Có cần chạy 5 trình kiểm tra kiểu dữ liệu Python cùng lúc không?
Mypy, Pyrefly, Pyright, ty, Zuban và có thể còn nhiều cái tên khác sẽ xuất hiện trong tương lai... Những người duy trì thư viện (library maintainers) nên đối mặt với tình trạng này như thế nào?
Tóm tắt nhanh: Hãy ưu tiên chạy càng nhiều trình kiểm tra kiểu dữ liệu (type-checker) càng tốt trên bộ kiểm thử (test suite) của bạn. Ít nhất, hãy chạy một công cụ trên mã nguồn.
Kiểm tra kiểu dữ liệu nào là quan trọng nhất (và tại sao bạn có thể đang làm ngược)
Nếu bạn chỉ đọc một phần của bài viết này, hãy đảm bảo đó là phần này. Bởi vì đây là nơi nhiều gói phần mềm mắc sai lầm. Rất phổ biến khi thấy các gói chạy type-checker trên mã nguồn của họ nhưng lại để bộ kiểm thử không có kiểu dữ liệu (untyped). Cách tiếp cận đó thực sự là ngược chiều.
Giả sử bạn duy trì một gói Python. Với tư cách là một người dùng giả định của mã của bạn, tôi không thực sự quan tâm đến các quy trình phát triển nội bộ của bạn. Bạn dùng ruff format hay black, cách bạn sắp xếp các lệnh nhập (import), bạn dùng pytest hay unittest — không điều nào trong số đó ảnh hưởng đến tôi. Điều tôi quan tâm là API công khai của bạn và trải nghiệm của tôi khi tương tác với nó.
Khi bạn chạy một type-checker trên mã nguồn nội bộ, bạn chủ yếu đang kiểm tra logic nội bộ. Bạn có thể làm việc đó với bất kỳ type-checker nào bạn thích, đó là lựa chọn của bạn. Tuy nhiên, type-checker nào mà người dùng của bạn sử dụng thì không phải là lựa chọn của bạn.
Bằng cách chạy càng nhiều type-checker càng tốt trên bộ kiểm thử, bạn đảm bảo rằng API công khai của gói hoạt động tốt với càng nhiều người dùng càng tốt.
Câu chuyện của Polars
Polars là một thư viện dataframe hiện đại, từ khi ra mắt năm 2020, nó đã tạo ra cơn sốt trong thế giới khoa học dữ liệu. Là một người dùng nặng của thư viện này, tôi rất quan tâm đến việc cải thiện trải nghiệm của nhà phát triển. Nếu các kiểu dữ liệu của Polars chính xác, tôi sẽ nhận được tính năng tự động hoàn thành (auto-complete) tốt hơn, tài liệu tốt hơn và sự bảo vệ khỏi một số lớp lỗi. Việc thêm Pyrefly vào các công việc tích hợp liên tục (CI) của Polars cần những gì?
Tôi bắt đầu điều tra và nhanh chóng gặp phải một số trở ngại. Pyrefly thường nghiêm ngặt hơn mypy, vì vậy nó yêu cầu viết lại một phần cơ sở mã hoặc thêm chú thích kiểu dữ liệu rõ ràng hơn khi khởi tạo biến. Hơn nữa, tôi gặp một số lỗi trong Pyrefly, và đáng khích lệ là bản sửa lỗi cho phần lớn chúng đã được đưa ra trong bản phát hành v1 được mong đợi. Tôi nghĩ rằng điều đó là xứng đáng, đặc biệt là nó đã phát hiện ra một lỗi mức độ trung bình, nhưng tôi đã phải tự hỏi liệu có nên làm điều tương tự cho ba type-checker khác hay không.
Để minh họa điểm này, hãy xem hàm DataType.__eq__. Trong Python, bất kỳ phương thức __eq__ nào cũng được mong đợi trả về bool, và nếu không, chúng ta cần phải nói rõ với type-checker để bỏ qua lỗi kiểu. Hàm này trong Polars cũng có thể trả về các kiểu khác nhau tùy thuộc vào đầu vào, do đó yêu cầu sử dụng overloads. Để hàm này thỏa mãn cả mypy, Pyrefly và ty, chúng ta cần viết:
@overload # type: ignore[override]
def __eq__( # pyrefly: ignore[bad-override]
self, other: pl.DataTypeExpr
) -> pl.Expr: ...
@overload
def __eq__(self, other: PolarsDataType) -> bool: ...
def __eq__(self, other: pl.DataTypeExpr | PolarsDataType) -> pl.Expr | bool:
# ty: ignore[invalid-method-override]
# pyright: ignore[reportIncompatibleMethodOverride]
Wow, 4 chú thích type-ignore khác nhau cho chỉ 7 dòng code! Bạn có thể thấy cơ sở mã nhanh chóng trở nên lộn xộn với những chú thích như vậy, hoặc các giải pháp thay thế để xử lý các điểm kỳ quặc của các type-checker khác nhau. Tôi không nghĩ rằng bất kỳ người duy trì thư viện nào muốn một cơ sở mã trông như vậy. Chắc chắn phải có một cách tốt hơn?
Thay vì đưa tất cả các nội bộ của bạn qua nhiều type-checker, tại sao không bắt đầu bằng cách kiểm tra xem tất cả các type-checker chính đều có thể được sử dụng với API công khai của thư viện của bạn hay không? Điều đó hữu ích hơn nhiều, nên việc dành thời gian cho nó dễ dàng biện minh hơn. Nhưng nó cũng dễ dàng hơn, bởi vì bạn chỉ cần đảm bảo rằng nếu thư viện của bạn được sử dụng như dự định, thì không có lỗi kiểu nào xảy ra. Trong trường hợp của DataType.__eq__, có một bài kiểm tra cho nó trông như sau:
DTYPE_TEMPORAL_UNITS: Final[frozenset[TimeUnit]] = frozenset(["ns", "us", "ms"])
def test_dtype_time_units() -> None:
# check (in)equality behaviour of temporal types that take units
for time_unit in DTYPE_TEMPORAL_UNITS:
assert pl.Datetime == pl.Datetime(time_unit)
assert pl.Duration == pl.Duration(time_unit)
assert pl.Datetime(time_unit) == pl.Datetime
assert pl.Duration(time_unit) == pl.Duration
Điều đáng mừng là mypy, Pyrefly, Pyright, ty, Zuban đều kiểm tra kiểu tốt mà không báo bất kỳ lỗi nào! Vì vậy, mặc dù các type-checker không đồng ý một chút về cách triển khai nên được viết, nhưng chúng đều đồng ý về các tác động lên API công khai. Và đó là điều người dùng của bạn quan tâm!
Việc đưa Pyrefly chạy trên toàn bộ bộ kiểm thử của Polars tương đối không đau đớn, bạn có thể kiểm tra PR để xác minh điều này. Để dễ dàng cho sự phát triển nội bộ của Polars, chúng tôi cũng đang khám phá việc sử dụng Pyrefly trên mã nguồn của họ, mặc dù đó là một nỗ lực lớn hơn và đang được giải quyết từng bước.
Còn mã nguồn của tôi thì sao? Tại sao lại có quá nhiều type-checker?
Thông số kỹ thuật về kiểu dữ liệu (typing spec) phác thảo một tập hợp các quy tắc tiêu chuẩn mà các type-checker được mong đợi tuân thủ. Tuy nhiên, có những khía cạnh hơi mơ hồ, chẳng hạn như trong các trường hợp người dùng chỉ định thiếu thông tin kiểu. Trong những trường hợp đó, các type-checker khác nhau đưa ra các quyết định thiết kế khác nhau:
- Một số chọn cách càng nghiêm ngặt càng tốt, phát ra các dương tính giả (false-positives) nếu cần thiết, nhưng làm hết sức có thể để bảo vệ bạn khỏi các lỗi tiềm ẩn.
- Những cái khác nới lỏng hơn và cho phép bạn thêm thông tin kiểu vào cơ sở mã của mình một cách từ từ.
Khi đến việc kiểm tra kiểu cho mã nguồn của bạn, tốt hơn hết là hãy tự hỏi mình bạn muốn nằm ở đâu trên phổ từ nghiêm ngặt đến nới lỏng. Pyrefly không chỉ nghiêm ngặt (mặc dù điều này có thể được cấu hình) mà còn nhanh và tuân thủ chuẩn, khiến nó trở thành một lựa chọn tuyệt vời. Nếu bạn thử nó trên các dự án của mình và gặp bất kỳ vấn đề nào, hãy báo cáo chúng để cả bạn và tất cả người dùng khác của nó đều được hưởng lợi từ các bản sửa lỗi!
Kết luận
Hiện có 5 trình kiểm tra kiểu Python đang nhận được sự chú ý: mypy, Pyrefly, Pyright, ty, Zuban. Những người duy trì thư viện có thể cảm thấy đúng rằng việc chạy cả 5 công cụ này trên mã nguồn của họ là quá nhiều nỗ lực bảo trì và yêu cầu làm ô nhiễm mã của họ bằng quá nhiều chú thích type-ignore. Chúng tôi đã lập luận rằng nỗ lực như vậy sẽ được sử dụng tốt hơn bằng cách chạy nhiều type-checker trên các bài kiểm thử của họ thay thế, vì điều đó sẽ kiểm tra xem thư viện có được kiểm tra kiểu tốt như thế nào khi người dùng tương tác với nó.
