- uv có thế mạnh về tốc độ, quản lý phiên bản Python và tích hợp nhiều công cụ vào một binary duy nhất, nhưng UX quản lý gói ở giai đoạn bảo trì vẫn còn khá thô
- Có thể kiểm tra các gói cũ bằng
uv pip list --outdated, nhưng vì nằm dưới namespace tương thích pip thay vì là lệnh cấp cao nhất, nên khả năng được phát hiện khá thấp
uv add pydantic mặc định thêm ràng buộc không có cận trên như pydantic>=2.13.4, nên cả việc tăng major version cũng được cho phép
- Việc nâng cấp toàn bộ được xử lý bằng
uv lock --upgrade, còn khi nâng cấp nhiều gói cụ thể thì cần lặp lại --upgrade-package, khiến câu lệnh trở nên dài dòng
- Có thể dùng cấu hình
add-bounds = "major" để tạo ràng buộc mặc định an toàn hơn, nhưng đây vẫn là tính năng preview và ứng dụng vẫn cần UX cập nhật trực quan hơn
Điểm mạnh của uv và sự bất tiện trong giai đoạn bảo trì
- uv của Astral mạnh ở tốc độ cao, quản lý phiên bản Python và khả năng thay thế nhiều công cụ bằng một binary duy nhất
- Quy trình bắt đầu một dự án Python mới và thêm phụ thuộc đầu tiên thì khá dễ, nhưng khi dự án bước vào giai đoạn bảo trì, UX kiểm tra gói cũ và nâng cấp định kỳ lại cho cảm giác thô hơn pnpm hay Poetry
- Các bất tiện chính nằm ở khả năng phát hiện lệnh kiểm tra gói cũ, việc thiếu cận trên trong ràng buộc phiên bản mặc định và sự dài dòng của lệnh nâng cấp
Kiểm tra các gói đã cũ
- Trong các dự án JavaScript, có thể dùng
pnpm outdated để xem ngắn gọn các gói đã cũ, phiên bản hiện tại, phiên bản mới nhất và phiên bản được phép theo điều kiện ràng buộc
- uv không có lệnh cấp cao nhất
uv outdated, và ban đầu lệnh sau được dùng như một phương án thay thế
$ uv tree --outdated --depth 1
uv tree --outdated --depth 1 không chỉ lọc ra các mục đã cũ mà còn in ra toàn bộ cây phụ thuộc cấp cao nhất, rồi gắn một chú thích nhỏ cạnh những mục có thể cập nhật
- Dù có 50 phụ thuộc mà chỉ 2 gói đã cũ, bạn vẫn phải rà qua danh sách 50 dòng
poetry show --outdated của Poetry cũng có tên lệnh kém trực quan hơn, nhưng đầu ra thực tế chỉ hiển thị các gói đã cũ
Rủi ro của ràng buộc phiên bản mặc định
-
Cách mặc định của pnpm và Poetry
pnpm add ghi vào package.json một yêu cầu dạng caret như ^1.23.4
^1.23.4 cho phép các phiên bản 1.x.x nhưng không cập nhật lên 2.0.0
- Poetry cũng mặc định dùng dạng như
>=1.23.4,<2.0.0; cách viết kém dễ đọc hơn nhưng hiệu quả là như nhau
- Với hai công cụ này, nếu giả định gói tuân thủ SemVer thì ngay cả khi chạy
pnpm update hay poetry update, khả năng build bị hỏng do thay đổi API lớn cũng được giảm xuống
-
Cách mặc định của uv
uv add pydantic thêm vào pyproject.toml một ràng buộc không có cận trên như sau
dependencies = [
"pydantic>=2.13.4",
]
- Với ràng buộc này, các phiên bản pydantic 2, 3 hay 100 đều được chấp nhận
- Khi chạy cập nhật hàng loạt, bạn không chỉ nhận các bản vá lỗi mà còn có thể nhận cả những thay đổi phá vỡ tương thích do mọi maintainer trong đồ thị phụ thuộc phát hành
- Điều này đặc biệt có thể dẫn tới rủi ro về độ ổn định trong quá trình bảo trì ứng dụng
UX của lệnh nâng cấp
- Trong pnpm và Poetry, cập nhật toàn bộ rất đơn giản như sau
$ pnpm update
$ poetry update
- Trong uv, lệnh sau được dùng để nâng cấp toàn bộ
$ uv lock --upgrade
uv lock --upgrade không phải uv update hay uv upgrade mà hoạt động như một tùy chọn của lệnh lock, nên với góc nhìn là lệnh quản lý gói dành cho con người, nó kém trực quan hơn
- Khi kết hợp với ràng buộc không có cận trên,
uv lock --upgrade trở thành lựa chọn nâng tất cả các gói trong lockfile lên phiên bản mới nhất tuyệt đối
- Bản cập nhật này có thể bao gồm cả các phụ thuộc lồng sâu mà bạn thậm chí không trực tiếp biết tới
- Nếu chỉ muốn cập nhật một số gói cụ thể, pnpm cho phép liệt kê tên gói như sau
$ pnpm update pydantic httpx uvicorn
- Còn trong uv, bạn phải lặp lại cờ
--upgrade-package cho từng gói
$ uv lock --upgrade-package pydantic --upgrade-package httpx --upgrade-package uvicorn
- Khi cần nâng nhiều gói cùng lúc, việc lặp lại cờ này trở thành một phiền toái lớn
Cờ --bounds và cấu hình
- Gần đây, uv đã thêm tùy chọn
--bounds cho uv add
$ uv add pydantic --bounds major
- Lệnh này tạo ra ràng buộc an toàn hơn là
pydantic>=2.13.4,<3.0.0
--bounds major hiện vẫn là tính năng preview và là tùy chọn opt-in phải nhập thủ công cho từng lệnh
- Sau đó, người ta phát hiện có thể đặt giá trị mặc định một lần trong
pyproject.toml
[tool.uv]
add-bounds = "major"
- Với cấu hình này, bạn không cần nhập
--bounds major mỗi lần nữa mà các lệnh uv add sau đó sẽ có mặc định hợp lý hơn
- Với ứng dụng, sẽ tốt hơn nếu đây là hành vi mặc định, nhưng mức độ tiện dụng thực tế không tệ đến như mô tả ban đầu
Sự khác biệt giữa ứng dụng và thư viện
- Lời khuyên chuẩn trong hệ sinh thái đóng gói Python là thư viện phát hành lên PyPI không nên cố định cận trên, và lời khuyên này là hợp lý
- Nếu mọi thư viện đều cố định cận trên, cây phụ thuộc của downstream consumer có thể không giải được
- Ngược lại, ứng dụng là nút cuối của đồ thị phụ thuộc và không có người dùng khác giải phụ thuộc dựa trên các ràng buộc đó
- Với ứng dụng, việc đặt cận trên không có chi phí nhưng lại giúp bảo vệ khỏi những lần tăng major version ngoài dự kiến
- Phạm vi ở đây là việc bảo trì các ứng dụng như website, dịch vụ hay công cụ nội bộ; còn với việc phát hành thư viện thì mặc định không có cận trên có thể là hợp lý
Những phần đã được đính chính và các vấn đề còn lại
- Có thể dùng
uv pip list --outdated để chỉ xem các gói đã cũ
$ uv pip list --outdated
- Vì vậy, phê phán về đầu ra quá ồn của
uv tree --outdated --depth 1 đã yếu đi
- Vấn đề còn lại là tính năng này không phải lệnh cấp cao nhất mà nằm dưới namespace tương thích
pip, nên khả năng được phát hiện vẫn thấp
- Có thể dùng cấu hình
add-bounds = "major" để đặt bounds mặc định, nên cách nhìn rằng hoặc phải tự tay chỉnh cận trên mọi lần hoặc phải chấp nhận rủi ro là không hoàn toàn đúng
- Dù vậy, đây vẫn là tính năng preview, và trong quản lý gói cho ứng dụng vẫn cần ràng buộc mặc định an toàn hơn cùng lệnh cập nhật trực quan hơn
Hướng cải thiện mong muốn
- Cần một lệnh
uv outdated chuyên dụng hiển thị rõ ràng chỉ các gói đã cũ
- Cần một lệnh
update công thái học hơn để cập nhật nhiều gói mà không phải lặp lại cờ
- Ràng buộc phiên bản mặc định nên phản ánh tốt hơn kỳ vọng ổn định của semantic versioning (SemVer)
- Ở trạng thái hiện tại, gánh nặng phải nghi ngờ và kiểm tra từng dòng thay đổi trong lockfile vẫn còn đó
1 bình luận
Ý kiến trên Hacker News
Phạm vi phiên bản mặc định của
uv addcó thể được đặt thành cấu hình cố định nên không cần truyền mỗi lầnTham khảo: https://docs.astral.sh/uv/reference/settings/#add-bounds
Lý do mặc định không thêm cận trên là vì điều đó tạo ra rất nhiều xung đột không cần thiết trong hệ sinh thái, và hồi còn dùng Poetry tôi cũng từng tổng hợp tài liệu liên quan ở đây: https://github.com/zanieb/poetry-relax#references
Khi dùng dependency trong một dự án web, tôi muốn có cận trên để ngăn thay đổi phá vỡ, với giả định dependency tuân thủ SemVer
Họ khuyến khích không thêm cận trên phòng thủ, liên tục build một phần lớn và năng động của hệ sinh thái ở mỗi bản phát hành để tìm ra vấn đề tương thích thực tế, tự động gửi thông báo cho chủ sở hữu, và cung cấp lịch trình rõ ràng để được giữ lại trong bản phát hành "LTS" tiếp theo
Giờ đây chỉ riêng bộ giải Cabal dường như cũng đã khá ổn định, nhưng các bản build nightly trên phạm vi rộng cùng những lỗi và chặn hiển thị rõ ràng hẳn đã giúp hệ sinh thái tiếp tục giữ được khả năng giải quyết phụ thuộc
add-bounds, và việc khóa chính xác dependency là quan trọng, nhưng nó cũng hữu ích cho các dự án mà lập trình viên ít kinh nghiệm dễ bỏ sót, đặc biệt là sản phẩm hoàn chỉnh chứ không phải thư viện--native-tlskhôngVì cấu hình Zscaler ở công ty nên UV luôn thất bại nếu không có cờ này
Tôi cũng muốn biết liệu có kế hoạch hỗ trợ chỉ định phiên bản Python tương thích cho một kiến trúc cụ thể hay không. Có một gói do công ty tôi duy trì bắt buộc phải dùng Python 32-bit nên lúc nào tôi cũng phải truyền
--python /path/to/32bitexclude-newertrongpyproject.tomlkhôngKhi chạy
uv run,exclude-newerbị xóa khỏipyproject.tomlTôi có thể chạy
uv run —-frozenhoặcuv run --exclude-newermỗi lần, nhưng điều đó không có vẻ là luồng làm việc đúng đắn, nên tôi muốn biết liệu có cách làm mang tính quy ước nào mà tôi đang bỏ sót khônguvđược thiết kế không có cận trên vì nó cần một kết quả giải duy nhấtnpm có thể cài các kết quả giải khác nhau ở những phần khác nhau của cây phụ thuộc, nhưng trong Python thì đó không phải một lựa chọn. Với Rye cũng phải đưa ra quyết định tương tự, và không có giải pháp nào tốt hơn
Nếu thêm cận trên thì trên thực tế có thể xuất hiện những cây phụ thuộc không còn giải được nữa. Một số hệ sinh thái package của Python trước đây thậm chí đã phát hành override cho các package cũ được phát hành với giả định sai về cận trên
Ở thời điểm hiện tại, bạn không thể biết package của mình có tương thích với một package chưa được phát hành hay không
uvbáo lỗi rằng các package không tương thích và tôi có thể override khi cầnNhư vậy vẫn tốt hơn là gặp lỗi không tương thích phiên bản lúc runtime mà rất khó lần theo
pyproject.tomlkhông phải vấn đề thật sựVấn đề thật sự là
uv lock —-upgradenâng cấp hàng loạt mọi thứ không có cận trênNếu có cách nâng cấp package mà không tăng major version thì lệnh này sẽ an toàn hơn nhiều
So với thời trước uv thì tốt hơn rất nhiều, nhưng để mọi thứ thật sự ổn có lẽ sẽ cần cả hệ sinh thái chấp nhận một mức độ thay đổi phá vỡ nhất định. Nghĩ đến giai đoạn chuyển từ Python 2 sang 3 thì có vẻ cũng chưa ai mặn mà với kiểu thay đổi đó trong tương lai gần
Cờ
—-boundcó ích, nhưng lại là thêm một thứ phải gõ và phải nhớNếu uv có thể biết dự án đó không phải thư viện thì có lẽ mặc định thêm cận trên cũng là một cách
=rồi chỉ cập nhật thủ công, đừng tin rằng các bản cập nhật không tăng major sẽ không làm hỏng gìTôi có 257 dependency Python trong một ứng dụng đang chạy production, trong đó hơn một nửa là dependency trực tiếp
Tôi không đặt cận trên trong
pyproject.toml, và cứ mỗi 2 tuần lại chạyuv lock --upgradebằng GitHub ActionsVì test coverage tốt nên nếu có gì hỏng thì test sẽ fail, và còn có cả quy trình review có AI hỗ trợ. Khi PR nâng cấp được tạo ra, một workflow AI sẽ dùng script Python để liệt kê các bản cập nhật major và minor, tìm changelog rồi gắn link và tóm tắt, đồng thời phân tích mức độ rủi ro của từng package dựa trên cách package đó được dùng trong codebase
Nhìn chung hầu như không đau đớn gì, và không cần nâng cấp từng package một, kiểm tra package cũ hay xử lý dependency bị bỏ quên. Trường hợp hiếm hoi cần sửa từ phía tác giả dependency nên không thể giải quyết trong code của mình chỉ xảy ra khoảng 1 lần mỗi năm. Trong 3 tháng qua đã có 18 lần tăng major version, và chỉ một lần cần thay đổi code
Tôi cũng muốn làm vậy ở frontend, nhưng test chưa đủ để chạy an toàn. Test ở backend dễ viết hơn và quan trọng hơn, nên tôi nghĩ mọi codebase đều nên có. Nếu có test, bạn có thể cứ thế tự động nâng cấp toàn bộ
Ít nhất thì chúng rất giỏi trong việc biến chỉ dẫn ngôn ngữ tự nhiên thành test chính xác
Tôi đã lâu không còn phải tự viết test bằng tay, mà ngày xưa đó luôn là việc tôi hay than phiền, giờ thì không còn nữa
UV đã làm được nhiều điều cho Python, nhưng hôm nay tôi đã phải vật lộn khá nhiều
Tôi đang cố quản lý tập trung các script nằm rải rác ở nhiều repo và theo thời gian đã được triển khai theo những cách khác nhau
Cách tôi nghĩ tới là
uv run --with $package main --help, và tôi muốn nó tự động 1) cài rồi chạy nếu chưa có, 2) không cài nếu đã là bản mới nhất, 3) nếu chưa mới nhất thì cập nhậtNhưng cả ba điều đó đều khó hơn mong đợi. Về cơ bản
uv runcài lại mỗi lần, và mất 6 giây cho virtualenv cùng việc cài đặtuvxhayuv toolcũng không khá hơn bao nhiêu, mà còn phát sinh vấn đề mới là người dùng không nhận được nâng cấpCuối cùng tôi viết script để gọi paginated GET tới CodeArtifact, nếu có bản non-dev mới hơn thì cập nhật rồi chạy lại. Nó hoạt động, và độ trễ 200ms vẫn tốt hơn 6 giây, nhưng đó không phải trải nghiệm tôi mong muốn
uv run --with $package main --helplẽ ra phải cho đúng hành vi bạn nói với rất ít overheadNó không cài lại mỗi lần, và môi trường
--withđược giữ trong cache. Kể cả khi môi trường chưa được cache thì dependency vẫn được cache, và cài từ cache là rất nhanh. Chắc chắn phải dưới 200msNếu bạn mở một ví dụ tái hiện chi tiết thì chúng tôi có thể xem thử
uv tool installvàuv tool upgradecó vẻ phù hợpDù vậy, kiểu khó chịu nhỏ này có lẽ giải quyết khá dễ, nên nếu bạn mở issue thì sẽ rất tốt
uv run mainKhi đó nó sẽ tự động cài, cache và chạy các dependency cần thiết: https://docs.astral.sh/uv/guides/scripts/
uv tool upgradelà được?Tôi không chắc đó có đúng y hệt use case này không, nhưng nó từng rất hiệu quả với tôi trong việc đồng bộ hệ sinh thái microservice polyrepo
Tôi khá ngạc nhiên khi thấy người ta khuyên dùng
"uv tree --outdated --depth 1"để xem danh sách dependency cũCá nhân tôi từ khi tính năng này ra đời đến giờ vẫn dùng
"uv pip list --outdated"Dù vậy tôi đồng ý rằng một lệnh quan trọng đến mức này xứng đáng có subcommand cấp cao nhất riêng
Đúng là
"uv pip list --outdated"cho đầu ra tốt hơn nhiều, cảm ơn vì điều đóNhưng việc có tới 2 cách để xem package cũ mà đầu ra lại khác biệt lớn cũng khiến tôi cảm thấy UX thật tệ
"uv tree -od1"có lẽ cũng chạy đượcTuy vậy, một lời phê bình tương tự với các trình quản lý package như pacman là các lệnh dùng thường xuyên nên có câu lệnh dễ hiểu với con người như apt
So với chữ “tệ” trong tiêu đề thì ví dụ đưa ra chỉ ở mức phải thêm vài tham số nữa thôi
Có lẽ tiêu đề phù hợp hơn sẽ là những cải thiện chất lượng sống tôi muốn có ở UV
Bản thân phản hồi thì hữu ích và tôi đồng ý với phần lớn, nhưng kiểu diễn đạt đó làm giảm giá trị của phản hồi và dễ gây phản ứng phòng thủ
Cá nhân tôi cũng thấy giao diện dòng lệnh của uv hơi phiền, nhưng tôi hiểu tại sao nó lại được viết như thế
uvrất tuyệt, nhưng vấn đề lớn nhất của Python packaging hiện nay vẫn là xử lý tốt packaging cho khoa học/máy họcNếu muốn cài PyTorch thì trước hết phải xem là phiên bản nào, có CUDA hay không. Nếu có CUDA thì lại có 6 biến thể theo từng phiên bản CUDA, mà wheel còn quá lớn để đưa lên PyPI nên phải tải trực tiếp
Conda chỉ giải quyết phần nào bài toán này. Spack thì cấu hình được cực kỳ sâu và có thể chuẩn bị đủ dependency C/C++/Fortran cùng toolchain compiler cần thiết để đạt hiệu năng tối đa, nhưng lại không tích hợp tốt với uv và các công cụ tương tự. Vì thế rất khó đưa các dự án machine learning thử nghiệm do nhà nghiên cứu tạo ra lên production
Rồi cuối cùng vẫn quay về tình huống đã nói ban đầu
Có khá nhiều phản hồi hữu ích, nhưng cũng lẫn chút giật tít
Về
pnpm outdated, đây không phải điều tôi thấy được nhắc nhiều, nhưng nghe như một yêu cầu hợp lý. Có lẽ nó đến từ khác biệt văn hóa giữa Python và JavaScript. Với dependency Python, trừ khi nó có lỗ hổng hoặc bị hỏng, tôi gần như chưa bao giờ quan tâm nó có cũ hay không; còn trong hệ sinh thái JavaScript, có vẻ việc tranh thủ nâng cấp khi có dịp là chuyện khá phổ biến. Điều này không có nghĩa là xấu, mà cho thấy trực giác về việc nên đưa gì ra giao diện dòng lệnh có thể khác nhau đến mức nào giữa các cộng đồng lập trình lớnNhư Armin đã nói, hành vi cận trên của uv là có chủ đích, và về mặt chức năng là cần thiết theo cách Python giải quyết dependency. Đó là một đánh đổi mà Python chọn so với các ngôn ngữ khác, nhưng tôi thấy đây là đánh đổi tốt ở chỗ mỗi dependency trong cây chỉ có một bản duy nhất và mọi yêu cầu ràng buộc lẫn nhau đều được giải trên cơ sở đó
Lý do
uv lock --upgradehoạt động như vậy là vì nó nâng cấp lock file, chứ không phải các yêu cầu do người dùng khai báo. Trong khi đópnpm updatedường như nâng cấp các yêu cầu người dùng trongpackage.json. Có thể gây nhầm lẫn, nhưng đặt nó dướiuv lockthì chính xác hơn. Nếu không, sẽ có người thắc mắc vì saouv upgradelại không nâng cấp theo kiểu mà họ nghĩ. Dù vậy vẫn còn dư địa để trình bày gọn gàng hơn, và rõ ràng đã có nhu cầu từ người dùng về một subcommand uv chuyên nâng cấp trực tiếp các yêu cầuhttps://news.ycombinator.com/item?id=48230048
Việc lệnh
uv lockchỉ xử lý lock file là hợp lý, nhưng người dùng có nhu cầu thực tế là nâng cấp dependency trực tiếp và bắc cầu. Dependency bắc cầu thì làm được bằnguv lock --upgrade-package, nhưng hơi dài dòng. Nó cũng hoạt động với dependency trực tiếp, nhưng không đụng tớipyproject.toml, trong khi file này lại dễ được lập trình viên nhìn thấy hơn nhiềuNếu phiên bản package trong
uv.lockđi trướcpyproject.toml, thìpyproject.tomlsẽ kém đáng tin hơn với vai trò là bản hướng dẫn để hiểu bề mặt dependency. Sẽ thật tốt nếu có một lệnhuv upgradethân thiệnCái bẫy UX lớn nhất tôi thấy ở uv cho đến nay là
uv pip. Nhiều dự án dùng uv đúng cách vớipyproject.tomlvàuv.locktrong phát triển, nhưng trong Dockerfile triển khai hoặc công cụ CI lại dùnguv pip install -r pyproject.toml, tức là đi vòng quauv.lockViệc các coding agent có quá nhiều dữ liệu huấn luyện chứa
pipnên hay đề xuất những mẫuuv piptệ cũng là vấn đề, nhưng uv cũng nên cung cấp cơ chế bảo vệ người dùngTheo tôi uv là một công cụ tuyệt vời và nên được dùng rộng rãi hơn: https://aleyan.com/blog/2026-why-arent-we-uv-yet
poetry updatecũng cập nhật lock file. Tôi thấy cách tổ chức CLI của uv khá phiền để làm việc. Nó tạo cảm giác được thiết kế cho độ chính xác và cho máy móc, chứ không phải cho sự thân thiện với người dùnguv pip list --outdateduv upgradecó trong roadmapLý do nó chưa xuất hiện là vì rất khó tạo ra một trải nghiệm thực sự tốt, có nhiều sắc thái hơn mọi người tưởng rất nhiều, trong khi nhóm thì nhỏ và còn nhiều ưu tiên khác
pnpm outdatedlà xem điều gì sẽ được cập nhật nếu chạy"uv sync --update"hoặc"uv lock --update"Tuy nhiên, cũng có thể tốt hơn nếu những lệnh đó có prompt xác nhận
Pixi dùng uv làm backend, và tôi thích UI của nó vì rất dễ thêm alias tác vụ để liệt kê package cũ theo cách dễ đọc
Đặc biệt, Pixi-diff-to-markdown giúp việc rà soát các bản cập nhật package tự động trong CI dễ hơn nhiều
Nếu muốn xem trong số các package cũ thì những gì sẽ được cập nhật, bạn có thể tạo alias tác vụ như sau cho dự án
pixi task add outdated "pixi update --dry-run --json | pixi exec pixi-diff-to-markdown"Sau đó chỉ cần chạy
pixi run outdatedtrong dự ánĐầu ra là một bảng Markdown dễ đọc, chứa các package sẽ được cập nhật, phiên bản hiện tại, và phiên bản mới sẽ được cài bởi lệnh
pixi update. Dĩ nhiên, điều này còn tùy gu và bối cảnhGần đây script
envđã xuất hiện trong đường dẫn và cản trở việc dùng lệnh UNIXenvthông thườngHóa ra trình cài đặt uv tạo ra nó khi chạy lệnh dưới đây
curl -LsSf [https://astral.sh/uv/install.sh](<https://astral.sh/uv/install.sh>) | shNó cài vào
$HOME/.cargo/bin/, rồi tạo một script shell ở$HOME/.cargo/envđể thêm$HOME/.cargo/bin/vào đầuPATHnếu đường dẫn đó chưa có sẵnTôi thật khó hiểu nổi kiểu lập trình viên nào lại viết code ghi đè lệnh UNIX mặc định theo cách như vậy