Bạn có thực sự cần chạy 5 công cụ kiểm tra kiểu dữ liệu Python cùng lúc?
Bài viết thảo luận về áp lực mà các nhà bảo trì thư viện Python phải đối mặt khi có quá nhiều công cụ kiểm tra kiểu dữ liệu như Mypy, Pyrefly và Pyright. Tác giả đề xuất chiến lược tối ưu: thay vì chạy tất cả các công cụ này trên mã nguồn gốc, hãy áp dụng chúng vào bộ kiểm thử (test suite) để đảm bảo API công khai hoạt động tốt nhất cho người dùng.

Bạn có thực sự cần chạy 5 công cụ kiểm tra kiểu dữ liệu Python cùng lúc?
Mypy, Pyrefly, Pyright, ty, Zuban, và có thể còn nhiều hơn nữa trong tương lai... các nhà bảo trì thư viện nên đối phó như thế nào?
TL;DR: Ưu tiên chạy càng nhiều công cụ kiểm tra kiểu (type-checker) càng tốt trên bộ kiểm thử (test suite) của bạn. Chạy ít nhất một công cụ trên mã nguồn.
Kiểm tra kiểu dữ liệu nào thực sự quan trọng?
Nếu bạn chỉ đọc một phần của bài viết này, hãy để nó là phần này. Đây là nơi mà nhiều gói thư viện (package) thường mắc sai lầm. Rất phổ biến khi thấy các gói chạy các công cụ kiểm tra kiểu trên mã nguồn của họ nhưng lại để bộ kiểm thử (tests) không có định dạng kiểu. Cách tiếp cận này thực sự là ngược đời.
Giả sử bạn bảo 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 sử dụng ruff format hay black, cách bạn sắp xếp các lệnh nhập (import), hay 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 công cụ kiểm tra kiểu 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 điều đó với bất kỳ công cụ kiểm tra kiểu nào bạn thích, đó là lựa chọn của bạn. Tuy nhiên, công cụ kiểm tra kiểu 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 công cụ kiểm tra kiểu càng tốt trên bộ kiểm thử của mình, bạn đảm bảo rằng API công khai của gói hoạt động tốt cho càng nhiều người dùng của bạn càng tốt.
Câu chuyện của Polars
Polars là một thư viện dataframe hiện đại, kể từ khi ra mắt vào năm 2020, đã 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 làm cho trải nghiệm của nhà phát triển (developer experience) của nó tốt hơn. Nếu các kiểu dữ liệu của Polars chính xác, thì với tư cách là người dùng, 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 nhất định. Vậy cần gì để thêm Pyrefly vào các công việc tích hợp liên tục (CI) của Polars?
Tôi bắt đầu điều tra vấn đề này 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 (type annotations) 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à các 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 chờ lâu dà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 độ ưu tiên trung bình, nhưng tôi đã phải tự hỏi liệu việc làm điều này cho thêm ba công cụ kiểm tra kiểu nữa có đáng không.
Để minh họa cho đ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õ cho các công cụ kiểm tra kiểu biết để 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 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, đó là 4 nhận xét bỏ qua kiểu (type-ignore comments) khác nhau cho chỉ 7 dòng mã! Bạn có thể thấy một cơ sở mã nhanh chóng trở nên lộn xộn với các nhận xét như vậy, hoặc với các giải pháp thay thế để xử lý các kỳ quặc của các công cụ kiểm tra kiểu khác nhau. Tôi không nghĩ rằng bất kỳ nhà bảo 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 công cụ kiểm tra kiểu, tại sao không bắt đầu bằng cách kiểm tra xem tất cả các công cụ kiểm tra kiểu 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, vì vậy việc dành thời gian cho nó dễ 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 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 vui mừng là mypy, Pyrefly, Pyright, ty, Zuban đều kiểm tra kiểu này tốt mà không báo bất kỳ lỗi nào! Vì vậy, mặc dù các công cụ kiểm tra kiểu không đồng ý một chút về cách triển khai nên được viết, nhưng tất cả chúng đều đồng ý về các tác động đối với 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 chính 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.
Vậy còn mã nguồn của tôi thì sao? Tại sao lại có nhiều công cụ kiểm tra kiểu đến vậy?
Thông số kỹ thuật về kiể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 công cụ kiểm tra kiểu đượ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 thông tin kiểu không đầy đủ. Trong những trường hợp đó, các công cụ kiểm tra kiểu 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 dần dần.
Khi đến việc kiểm tra kiểu mã nguồn của bạn, nên tự hỏi bản thân xem 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ủ, 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ó có thể hưởng lợi từ các bản sửa lỗi!
Kết luận
Hiện có 5 công cụ kiểm tra kiểu Python đang nhận được sự chú ý: mypy, Pyrefly, Pyright, ty, Zuban. Các nhà bảo 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 nhận xét 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 công cụ kiểm tra kiểu 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ó thể được kiểm tra kiểu tốt như thế nào khi người dùng tương tác với nó.
