Python hiện đại 4 — uv & ruff: môi trường và chất lượng code
Nỗi đau cũ: môi trường Python rời rạc và chậm
Ai từng dựng môi trường Python cho một dự án data đều nhớ cảm giác này: bạn tạo virtualenv bằng python -m venv, kích hoạt nó, rồi pip install -r requirements.txt. Chờ. Chờ tiếp. pip tải từng gói, giải quyết phụ thuộc (dependency resolution) theo kiểu quay lui chậm chạp, biên dịch vài gói native, và mười phút sau bạn mới có môi trường chạy được. Khi đồng nghiệp clone repo về, họ nhận được một requirements.txt chỉ ghi tên gói chứ không ghi phiên bản chính xác của toàn bộ cây phụ thuộc — nên máy họ cài ra một tổ hợp khác máy bạn. Đến lúc lên CI hay đóng Docker image lại ra tổ hợp thứ ba. Lỗi "chạy được trên máy tôi" sinh ra từ đây.
Bức tranh còn phân mảnh hơn ở tầng công cụ. Để quản lý dự án, người thì dùng pip thuần, người dùng poetry, người dùng pipenv, người dùng pdm — mỗi công cụ một file cấu hình, một lockfile, một triết lý riêng. Để giữ code sạch, một dự án điển hình cài tới bốn công cụ: black để format, isort để sắp thứ tự import, flake8 để bắt lỗi phong cách, và pylint để phân tích sâu hơn. Bốn công cụ này cấu hình ở bốn chỗ, đôi khi đánh nhau (black format một kiểu, isort muốn kiểu khác), và mỗi lần chạy đủ bộ trên một repo lớn mất vài chục giây tới vài phút. Cài pip install black isort flake8 pylint mypy xong, môi trường phình thêm hàng chục gói phụ.
Đây chính là mảnh ghép mà series này đã hẹn ở bài tổng quan: làn sóng công cụ Rust không chỉ tăng tốc phần xử lý dữ liệu (Polars, DuckDB) mà còn dọn dẹp luôn phần tooling. Hai cái tên trung tâm là uv và ruff, cùng do công ty Astral viết bằng Rust.
uv: một trình quản lý gói và môi trường, thay cho cả rừng công cụ
uv là trình quản lý gói và môi trường Python, viết bằng Rust, đặt mục tiêu thay thế cùng lúc pip, virtualenv/venv, pip-tools, và pipx — thậm chí quản lý luôn việc cài đặt bản thân trình thông dịch Python. Điểm gây choáng đầu tiên là tốc độ: uv thường nhanh hơn pip hàng chục lần khi cài đặt, đặc biệt khi cache đã ấm.
Vì sao uv nhanh
Ba lý do chính, không phải phép màu:
- Viết bằng Rust, chạy song song. Việc tải và giải nén gói được thực hiện đa luồng thay vì tuần tự như pip.
- Bộ giải phụ thuộc (resolver) hiện đại. uv dùng thuật toán PubGrub — cùng họ với resolver của các trình quản lý gói đời mới — cho phép tìm tổ hợp phiên bản tương thích nhanh và báo lỗi rõ ràng khi có xung đột, thay vì quay lui mù quáng.
- Cache toàn cục có chia sẻ cứng (hard-link). Mỗi phiên bản gói chỉ tải và lưu một lần trên máy; các môi trường ảo khác nhau liên kết cứng tới cùng file trong cache thay vì sao chép lại. Tạo môi trường mới gần như tức thì và tiết kiệm ổ đĩa.
Các lệnh cốt lõi
Bảng dưới tóm tắt những lệnh dùng hằng ngày (minh hoạ):
| Lệnh | Việc nó làm |
|---|---|
uv init | Khởi tạo dự án mới: sinh pyproject.toml, cấu trúc thư mục cơ bản |
uv venv | Tạo virtualenv (mặc định thư mục .venv) |
uv add polars | Thêm một phụ thuộc vào pyproject.toml, cài nó, và cập nhật lockfile |
uv remove polars | Gỡ phụ thuộc và cập nhật lại lock |
uv sync | Đồng bộ môi trường đúng khớp với lockfile — cài cái thiếu, gỡ cái thừa |
uv lock | Giải phụ thuộc và ghi ra uv.lock (lockfile tái lập) |
uv run script.py | Chạy lệnh/script trong môi trường dự án, tự đảm bảo môi trường đã đồng bộ |
uv python install 3.12 | Tải và cài một bản Python độc lập, không đụng Python hệ thống |
Điểm quan trọng nhất về mặt vận hành là cặp uv.lock + uv sync. Khi bạn chạy uv lock, uv giải toàn bộ cây phụ thuộc và ghi lại phiên bản chính xác cùng mã băm (hash) của mọi gói — cả gói trực tiếp lẫn gói phụ thuộc gián tiếp. File uv.lock này được commit vào git. Bất kỳ ai chạy uv sync sau đó — trên laptop khác, trong CI, trong Docker — đều dựng ra môi trường giống hệt từng byte. Đây là "tái lập được" (reproducible) đúng nghĩa, thứ mà requirements.txt viết tay không bao giờ đảm bảo.
Chạy script với phụ thuộc nội tuyến
Một tính năng gọn mà rất hữu ích cho các script phân tích dùng một lần: uv hỗ trợ khai báo phụ thuộc ngay trong file script theo chuẩn PEP 723. Bạn viết một khối chú thích đặc biệt ở đầu file, rồi uv run sẽ tự dựng môi trường tạm có đúng các gói đó rồi chạy — không cần tạo project hay venv thủ công:
# /// script
# dependencies = ["polars", "duckdb"]
# ///
import polars as pl
print(pl.__version__)
Chạy bằng uv run phan_tich.py. Rất tiện cho các đoạn ETL nhỏ hay notebook chuyển thành script trong ngân hàng, khi bạn muốn gửi đồng nghiệp một file duy nhất mà họ chạy được ngay.
ruff: linter và formatter gộp làm một
ruff là công cụ kiểm tra và định dạng code Python, cũng viết bằng Rust, thay thế cùng lúc black (formatter), isort (sắp import), flake8 (linter phong cách) và phần lớn pylint. Nó nhanh tới mức trên một codebase cỡ lớn, ruff quét xong trong khi black cũ còn chưa khởi động xong — thường nhanh hơn hàng chục tới hàng trăm lần so với chuỗi công cụ cũ tương ứng.
Hai lệnh chính:
ruff check— chạy linter: bắt hàng trăm loại lỗi và mùi code (unused import, biến chưa dùng, so sánh sai kiểu, import không sắp thứ tự...). Thêm--fixđể tự động sửa những gì sửa được.ruff format— chạy formatter, tương thích gần như tuyệt đối với phong cách black.
ruff sở hữu hàng trăm rule, được gom thành nhóm theo tiền tố chữ cái (ví dụ E/W cho pycodestyle, F cho pyflakes, I cho isort, B cho flake8-bugbear, UP cho pyupgrade...). Bạn bật/tắt từng nhóm trong cấu hình. Rất nhiều rule có autofix: ruff không chỉ báo lỗi mà còn sửa hộ, an toàn và nhất quán.
Cái được lớn nhất không chỉ là tốc độ mà là hợp nhất: một công cụ, một chỗ cấu hình, không còn cảnh black và isort cãi nhau. Cả linter lẫn formatter dùng chung một cách hiểu về code nên kết quả luôn ăn khớp.
pyproject.toml: trung tâm của mọi thứ
Sợi chỉ nối uv và ruff lại là file pyproject.toml. Đây là file cấu hình chuẩn của Python hiện đại (định nghĩa bởi các PEP như 517/518/621), thay cho setup.py kiểu cũ. Nó gom vào một chỗ:
- Metadata dự án: tên, phiên bản, mô tả, phiên bản Python yêu cầu.
- Danh sách phụ thuộc: chính và theo nhóm (ví dụ nhóm
devcho công cụ phát triển). - Cấu hình công cụ: ruff, pytest, mypy... mỗi cái một bảng
[tool.*].
Nhờ đó, thay vì rải cấu hình khắp setup.py, setup.cfg, .flake8, .isort.cfg, requirements.txt, bạn chỉ còn một file để đọc và review. Một ví dụ minh hoạ (không phải cấu hình bắt buộc, chỉ để thấy hình dạng):
[project]
name = "ncb-risk-etl"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"polars>=1.0",
"duckdb>=1.0",
"pydantic>=2.7",
]
[dependency-groups]
dev = ["ruff", "pytest", "mypy"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"] # pycodestyle, pyflakes, isort, bugbear, pyupgrade
[tool.pytest.ini_options]
testpaths = ["tests"]
Đọc file này là hiểu ngay dự án cần Python nào, dùng gói gì, format theo chuẩn nào. Đây là điểm khác biệt lớn với thời setup.py — một file thực thi được (tức là phải chạy mới biết nội dung), khó phân tích tĩnh và dễ giấu logic phức tạp.
Quy trình một dự án data hiện đại
Ghép lại thành luồng công việc chuẩn từ lúc khởi tạo tới lúc lên CI/Docker:
Diễn giải từng bước:
uv initkhởi tạo dự án, sinhpyproject.toml.uv add polars duckdbthêm các phụ thuộc data — uv tự cập nhậtpyproject.toml, cài gói và cập nhật lock.uv lockchốt lại lockfile; commit cảpyproject.tomllẫnuv.lockvào git.- Trong lúc code, chạy
ruff formatvàruff check --fixđể giữ code sạch và nhất quán — thường gắn vào pre-commit hook để tự chạy trước mỗi commit. - Trên CI, chạy
uv sync --frozen(chỉ dựng đúng theo lock, báo lỗi nếu lock lệch), rồiruff checklàm cổng chất lượng vàpytestchạy test. Xem thêm ở bài về testing và chất lượng. - Trong Docker, dùng uv để cài phụ thuộc — nhờ cache và lock, image build nhanh và tái lập. Astral cung cấp image nền có sẵn uv, và mẫu Dockerfile khuyến nghị copy
uv.locktrước rồiuv syncđể tận dụng layer cache.
Kết quả: cùng một tổ hợp phiên bản chạy trên laptop lập trình viên, trên CI, và trong container production — hết cảnh "máy tôi chạy được".
So với poetry, pdm — và vì sao uv+ruff đang thành mặc định
poetry và pdm là hai trình quản lý dự án đời trước uv, đều tốt hơn hẳn pip thuần: có lockfile, có quản lý nhóm phụ thuộc, cấu hình trong pyproject.toml. Nhưng cả hai viết bằng Python nên phần resolver và cài đặt vẫn chậm hơn nhiều so với uv viết bằng Rust. poetry giai đoạn đầu còn dùng cách khai báo phụ thuộc riêng (bảng [tool.poetry]) hơi lệch chuẩn PEP 621, gây khó khi di chuyển. uv bám sát chuẩn [project] và bổ sung tốc độ vượt trội cùng khả năng quản lý cả phiên bản Python — nên nó vừa là "poetry nhanh hơn" vừa làm được nhiều hơn.
Về phía lint/format, trước ruff người ta phải phối hợp black + isort + flake8 (+ plugin) + pylint. ruff gộp gần hết vào một binary, nhanh hơn nhiều bậc, cấu hình một chỗ. Sự tiện lợi đó khiến ruff lan rất nhanh: nhiều dự án lớn trong hệ sinh thái Python đã chuyển sang dùng.
Vì sao uv + ruff đang trở thành mặc định của 2024–2025? Tóm gọn: (a) tốc độ tạo khác biệt cảm nhận rõ hằng ngày; (b) hợp nhất nhiều công cụ giảm gánh nặng cấu hình và bảo trì; (c) bám chuẩn PEP nên không khoá bạn vào một hệ sinh thái riêng; (d) cùng một nhà (Astral) làm cả hai, phối hợp mượt. Đây không phải trào lưu nhất thời mà là sự hội tụ của cả cộng đồng về một bộ công cụ gọn hơn, nhanh hơn.
Cần nói thẳng một điểm để không bịa: uv và ruff còn khá trẻ, phát triển rất nhanh nên đôi khi có thay đổi giữa các phiên bản; vì thế việc ghim phiên bản công cụ trong dự án và CI là nên làm.
Use case thực tế
Bối cảnh (NCB): Team data của NCB có 6 người, mỗi người một laptop cấu hình khác nhau, cộng thêm một môi trường CI và các Docker image chạy job ETL rủi ro tín dụng ban đêm. Trước đây mỗi dự án dùng requirements.txt viết tay, không ghim phiên bản phụ thuộc gián tiếp. Hệ quả điển hình: một job tính toán chạy đúng trên máy phát triển nhưng lệch kết quả nhỏ trên production vì một thư viện phụ thuộc gián tiếp lên phiên bản mới, đổi hành vi làm tròn số.
Cách chuẩn hoá bằng uv + ruff:
- Mỗi repo chuyển sang
uv init, khai báo phụ thuộc bằnguv add, commitpyproject.toml+uv.lock. - Đặt cấu hình ruff chung trong
pyproject.toml, bật các nhóm ruleE, F, I, B, UP; gắnruff formatvàruff checkvào pre-commit hook. - CI chạy
uv sync --frozenđể đảm bảo môi trường khớp lock, rồiruff checklàm cổng chất lượng (fail là chặn merge) vàpytest. - Dockerfile copy
uv.locktrước,uv syncđể dựng đúng môi trường CI đã kiểm.
Số liệu ước lượng (minh hoạ, không phải đo chuẩn):
| Chỉ số | Trước (pip + venv, 4 công cụ lint) | Sau (uv + ruff) |
|---|---|---|
| Dựng môi trường sạch (cài ~40 gói) | ~3–5 phút | ~10–20 giây |
| Dựng lại khi cache ấm | ~1–2 phút | vài giây |
| Chạy lint + format toàn repo | ~30–60 giây | dưới 2 giây |
| Số công cụ lint/format phải bảo trì | 4 (black, isort, flake8, pylint) | 1 (ruff) |
| Môi trường tái lập giữa laptop/CI/Docker | Không đảm bảo | Giống hệt theo uv.lock |
Kết quả nghiệp vụ: hết lỗi "lệch phiên bản gián tiếp" nhờ lock; onboarding người mới rút từ nửa buổi loay hoay cài đặt xuống một lệnh uv sync; và mỗi pull request đều qua cùng một cổng chất lượng ruff nên code review tập trung vào logic thay vì cãi nhau về dấu cách. Với môi trường ngân hàng cần truy vết và tái lập được mọi phiên bản chạy production, khả năng lock chặt của uv là một điểm cộng về mặt kiểm soát.
Ghi nhớ
- uv (Rust, của Astral) là trình quản lý gói + môi trường siêu nhanh, thay cho
pip+venv+pip-tools+pipx, và quản lý được cả phiên bản Python. - Cặp
uv.lock+uv synccho môi trường tái lập giống hệt trên laptop, CI và Docker — commit lockfile vào git. - uv nhanh nhờ Rust đa luồng, resolver PubGrub, và cache toàn cục dùng hard-link.
- ruff (Rust, của Astral) gộp black + isort + flake8 + phần lớn pylint thành một công cụ:
ruff check(lint, có--fix) vàruff format; hàng trăm rule, nhiều rule autofix. pyproject.tomllà trung tâm: metadata + phụ thuộc + cấu hình tool ([tool.ruff],[tool.pytest...], mypy), thay chosetup.py.- Quy trình chuẩn:
uv init→uv add→uv lock→ruff format/check→ CI dùnguv sync --frozen+ ruff + pytest → Docker build bằng uv. - So với poetry/pdm (viết bằng Python): uv nhanh hơn nhiều, bám chuẩn PEP 621, làm được nhiều hơn — nên uv + ruff đang thành mặc định 2024–2025.
- Vì công cụ còn trẻ và thay đổi nhanh: ghim phiên bản uv/ruff trong dự án và CI.
Nguồn tham khảo
Bài viết liên quan
Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.
Biến script thành pipeline đáng tin cậy: cấu trúc project & packaging (uv/poetry), type hints & pydantic, kiểm thử với pytest, logging & cấu hình, đóng gói Docker, và tích hợp CI cho code dữ liệu.
Học cách tổ chức code Python: định nghĩa hàm với tham số vị trí/từ khoá/mặc định, *args/**kwargs, lambda và hàm bậc cao, closure, decorator, generator với yield. Đóng gói code thành module và package, cô lập thư viện bằng môi trường ảo venv, quản lý phụ thuộc với pip và requirements.txt để dự án tái lập được trên mọi máy.
Hướng dẫn OOP trong Python từ class/instance, kế thừa và super(), đa hình & duck typing, encapsulation tới dunder methods, @property, classmethod/staticmethod, dataclass và type hints (mypy). Kèm nguyên tắc clean code: đặt tên rõ nghĩa, hàm nhỏ, DRY, SOLID cùng chuẩn PEP8 với công cụ ruff/black.
Cảm nhận của bạn
Bình luận
Chưa có bình luận. Hãy là người đầu tiên chia sẻ!