Giao diện
pyproject.toml — Trung tâm Cấu hình Python Hiện đại
Hãy thử tưởng tượng: bạn vừa clone một dự án Python, chạy pip install -e . và nhận ngay lỗi ModuleNotFoundError: No module named 'setuptools'. Đây chính là bài toán con gà — quả trứng kinh điển của setup.py — bạn cần setuptools để đọc file cấu hình, nhưng file cấu hình lại định nghĩa phiên bản setuptools cần dùng. Thêm vào đó, setup.py là mã Python thực thi được — nghĩa là bất kỳ ai tạo package độc hại đều có thể chạy mã tùy ý trên máy bạn chỉ bằng lệnh pip install. Đó là lý do ngành công nghiệp đã chuyển sang pyproject.toml — một file khai báo tĩnh, an toàn, chuẩn hóa, giải quyết triệt để mọi vấn đề trên.
Insight nhanh: Kể từ PEP 621 (2021), bạn chỉ cần một file duy nhất — pyproject.toml — để định nghĩa metadata, dependencies, build system, và cấu hình mọi công cụ phát triển. Không còn setup.py, setup.cfg, MANIFEST.in rải rác khắp nơi.
Bức tranh tư duy
Hãy nghĩ pyproject.toml như bản thiết kế kiến trúc của một tòa nhà. Trước khi đổ móng (build), bạn cần một bản vẽ duy nhất ghi rõ: ai là chủ đầu tư (metadata), dùng vật liệu gì (dependencies), ai là nhà thầu thi công (build backend), và tiêu chuẩn kỹ thuật áp dụng (tool configs). Không ai xây nhà bằng cách viết script chạy mỗi lần đọc bản vẽ — đó chính là vấn đề của setup.py.
Hành trình tiến hóa
setup.py (2004) setup.cfg (2016) pyproject.toml (2021+)
───────────────── ───────────────── ─────────────────────
• Mã Python thực thi • Khai báo tĩnh (INI) • Khai báo tĩnh (TOML)
• Không parse được • Chỉ setuptools • Bất kỳ build backend
• Rủi ro bảo mật • Không chuẩn hóa • PEP 517/518/621
• Chicken-and-egg • Vẫn cần setup.py • Tự đứng độc lậpBản đồ cấu trúc pyproject.toml
pyproject.toml
├── [build-system] ← Nhà thầu thi công (PEP 517/518)
│ ├── requires Vật liệu cần trước khi xây
│ └── build-backend Tên nhà thầu chính
│
├── [project] ← Hồ sơ dự án (PEP 621)
│ ├── name, version Tên và phiên bản
│ ├── dependencies Phụ thuộc bắt buộc
│ ├── requires-python Phiên bản Python tối thiểu
│ ├── [project.optional-dependencies]
│ │ Phụ thuộc mở rộng (dev, docs, test)
│ ├── [project.scripts] Lệnh CLI entry points
│ └── [project.urls] Liên kết dự án
│
└── [tool.*] ← Cấu hình công cụ
├── [tool.pytest.ini_options] Pytest
├── [tool.ruff] Ruff linter
├── [tool.mypy] Type checker
└── [tool.setuptools] Build backend configCốt lõi kỹ thuật
[build-system] — Nền móng (PEP 517/518)
Mọi pyproject.toml bắt buộc phải có section này. Nó trả lời hai câu hỏi: cần cài gì trước khi build (requires) và ai sẽ thực hiện build (build-backend).
toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"PEP 518 giải quyết bài toán chicken-and-egg: pip đọc requires trước, cài các build dependencies vào môi trường cách ly, rồi mới gọi build-backend để build package. Không cần pre-install bất cứ thứ gì.
[project] — Metadata chuẩn hóa (PEP 621)
toml
[project]
name = "vn-payment-gateway"
version = "2.1.0"
description = "SDK xử lý thanh toán cho thị trường Việt Nam"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Nguyễn Văn An", email = "an.nguyen@example.com"},
]
keywords = ["payment", "vnpay", "momo", "zalopay"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]Version Constraints và SemVer
MAJOR.MINOR.PATCH
│ │ │
│ │ └── Sửa lỗi (tương thích ngược)
│ └──────── Tính năng mới (tương thích ngược)
└────────────── Thay đổi phá vỡ (KHÔNG tương thích ngược)toml
[project]
dependencies = [
# Chính xác — dùng cho ứng dụng triển khai, KHÔNG dùng cho thư viện
"package==1.2.3",
# Phiên bản tối thiểu
"package>=1.2.3",
# Khoảng an toàn — khuyến nghị cho thư viện
"package>=1.2.3,<2.0.0",
# Compatible release — tương đương >=1.2.0, <2.0.0
"package~=1.2",
# Loại trừ phiên bản lỗi đã biết
"package>=1.0.0,!=1.5.0",
# Điều kiện nền tảng (environment markers)
"pywin32>=306; sys_platform == 'win32'",
"typing-extensions>=4.0; python_version < '3.11'",
][project.optional-dependencies] — Nhóm phụ thuộc mở rộng
toml
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"ruff>=0.5.0",
"mypy>=1.10",
]
docs = [
"mkdocs>=1.6",
"mkdocs-material>=9.5",
]
performance = [
"uvloop>=0.19; sys_platform != 'win32'",
"orjson>=3.10",
]Cài đặt bằng: pip install vn-payment-gateway[dev,docs]
[project.scripts] — Entry Points
toml
[project.scripts]
vnpay-cli = "vn_payment.cli:main"
vnpay-migrate = "vn_payment.migrations:run"
[project.entry-points."vn_payment.plugins"]
vnpay = "vn_payment.backends.vnpay:VNPayBackend"
momo = "vn_payment.backends.momo:MoMoBackend"[tool.*] — Cấu hình công cụ phát triển
Thay vì rải file cấu hình khắp nơi (pytest.ini, .flake8, mypy.ini), tập trung tất cả vào pyproject.toml:
toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-ra -q --strict-markers --tb=short"
markers = [
"slow: đánh dấu test chạy lâu",
"integration: test tích hợp cần service ngoài",
]
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM"]
ignore = ["E501"]
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
disallow_untyped_defs = trueSo sánh Build Backends
| Tiêu chí | setuptools | hatchling | flit_core | maturin |
|---|---|---|---|---|
| Trường hợp dùng | Đa năng, legacy | Dự án hiện đại | Pure Python đơn giản | Rust extensions |
| Tốc độ build | Trung bình | Nhanh | Nhanh | Nhanh |
| C extensions | ✅ | ❌ | ❌ | ✅ (Rust) |
| Quản lý version | Thủ công/dynamic | Tích hợp sẵn | Từ __init__.py | Từ Cargo.toml |
| Quản lý env | ❌ | ✅ (hatch) | ❌ | ❌ |
| PEP 621 | ✅ (từ v61) | ✅ | ✅ | ✅ |
| Cấu hình tối thiểu | ~10 dòng | ~5 dòng | ~3 dòng | ~8 dòng |
toml
# === setuptools ===
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
# === hatchling ===
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# === flit_core ===
[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"
# === maturin (Rust + Python) ===
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"Thực chiến
Tình huống: Xây dựng dự án Python production từ đầu
Giả sử bạn đang xây dựng vn-payment-gateway — một SDK xử lý thanh toán. Đây là pyproject.toml hoàn chỉnh cho production:
toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "vn-payment-gateway"
dynamic = ["version"]
description = "SDK xử lý thanh toán đa cổng cho thị trường Việt Nam"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Payment Team", email = "payment@example.com"},
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
dependencies = [
"httpx>=0.27,<1.0",
"pydantic>=2.7,<3.0",
"cryptography>=42.0,<44.0",
"tenacity>=8.3,<10.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"pytest-asyncio>=0.23",
"ruff>=0.5.0",
"mypy>=1.10",
"pre-commit>=3.7",
]
docs = [
"mkdocs>=1.6",
"mkdocs-material>=9.5",
]
[project.scripts]
vnpay = "vn_payment.cli:main"
[project.urls]
Homepage = "https://github.com/example/vn-payment-gateway"
Documentation = "https://vn-payment-gateway.readthedocs.io"
Repository = "https://github.com/example/vn-payment-gateway"
Changelog = "https://github.com/example/vn-payment-gateway/blob/main/CHANGELOG.md"
[tool.hatch.version]
path = "src/vn_payment/__about__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/vn_payment"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-ra --strict-markers --tb=short"
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "ASYNC"]
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = trueMigration: Từ setup.py sang pyproject.toml
Bước 1 — Ánh xạ từng trường:
setup.py → pyproject.toml
──────────────────────────────── ────────────────────────────────
name="pkg" → [project] name = "pkg"
version="1.0" → [project] version = "1.0"
install_requires=[...] → [project] dependencies = [...]
extras_require={"dev": [...]} → [project.optional-dependencies]
entry_points={"console_scripts":} → [project.scripts]
packages=find_packages(where=) → [tool.setuptools.packages.find]
package_dir={"": "src"} → [tool.setuptools] package-dirBước 2 — Tạo pyproject.toml tương đương:
toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "vn-payment-gateway"
version = "1.0.0"
description = "SDK xử lý thanh toán"
readme = "README.md"
requires-python = ">=3.10"
authors = [
{name = "Payment Team", email = "payment@example.com"},
]
dependencies = [
"httpx>=0.27,<1.0",
"pydantic>=2.7,<3.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.5"]
[project.scripts]
vnpay = "vn_payment.cli:main"
[tool.setuptools.packages.find]
where = ["src"]Bước 3 — Kiểm tra build và xóa file cũ:
python
# validate_build.py — Chạy sau migration
import subprocess, sys
for name, cmd in [
("Build", [sys.executable, "-m", "build"]),
("Check", ["twine", "check", "dist/*"]),
]:
result = subprocess.run(cmd, capture_output=True, text=True)
status = "✓" if result.returncode == 0 else "✗"
print(f" {status} {name}")
if result.returncode != 0:
sys.exit(1)Sai lầm điển hình
❌ Sai lầm 1: Thiếu requires-python
Không chỉ định phiên bản Python tối thiểu khiến pip cài package trên Python cũ, gây lỗi runtime khó hiểu.
toml
# ❌ SAI — pip sẽ cài trên Python 3.8 mặc dù dùng match statement
[project]
name = "my-package"
dependencies = ["pydantic>=2.0"]
# ✅ ĐÚNG — pip từ chối cài trên Python không đủ phiên bản
[project]
name = "my-package"
requires-python = ">=3.10"
dependencies = ["pydantic>=2.0"]❌ Sai lầm 2: Dynamic version trỏ sai đường dẫn
toml
# ❌ SAI — module path không khớp cấu trúc thư mục
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.version.__version__"}
# → AttributeError: module 'my_package' has no attribute 'version'toml
# ✅ ĐÚNG — đảm bảo file src/my_package/__init__.py chứa __version__
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}python
# src/my_package/__init__.py — file PHẢI tồn tại
__version__ = "1.0.0"❌ Sai lầm 3: Thiếu package-data trong wheel
toml
# ❌ SAI — file JSON, template, py.typed biến mất sau khi build
[tool.setuptools]
packages = ["my_package"]
# ✅ ĐÚNG — khai báo rõ ràng file cần đóng gói
[tool.setuptools.package-data]
my_package = [
"py.typed",
"data/*.json",
"templates/*.html",
"locale/*/LC_MESSAGES/*.mo",
]❌ Sai lầm 4: Cấu hình tool mâu thuẫn
toml
# ❌ SAI — formatter và linter dùng line-length khác nhau
[tool.black]
line-length = 88
[tool.ruff]
line-length = 120 # → ruff cho pass, black format lại → vòng lặp vô hạn
# ✅ ĐÚNG — thống nhất giá trị xuyên suốt
[tool.black]
line-length = 88
[tool.ruff]
line-length = 88
[tool.ruff.format]
quote-style = "double" # Khớp với black mặc định❌ Sai lầm 5: Version constraints quá lỏng hoặc quá chặt
toml
# ❌ SAI — không ràng buộc → breaking change bất ngờ
dependencies = ["requests", "numpy"]
# ❌ SAI — pin cứng → không nhận bản vá bảo mật
dependencies = ["requests==2.31.0", "numpy==1.26.4"]
# ✅ ĐÚNG — khoảng linh hoạt với giới hạn major version
dependencies = [
"requests>=2.28,<3.0",
"numpy>=1.24,<2.0",
]Under the Hood
Build backend xử lý pyproject.toml như thế nào?
Khi bạn chạy python -m build, chuỗi sự kiện diễn ra theo trình tự:
python -m build
│
▼
┌──────────────────────────────────┐
│ 1. Đọc [build-system].requires │ ← pip parse TOML tĩnh
│ Cài build deps vào tmpenv │
├──────────────────────────────────┤
│ 2. Import build-backend module │ ← Gọi API chuẩn PEP 517
│ backend.build_wheel() │
├──────────────────────────────────┤
│ 3. Backend đọc [project] và │ ← Mỗi backend parse
│ [tool.<backend>] sections │ section riêng của mình
├──────────────────────────────────┤
│ 4. Tạo dist/*.tar.gz + *.whl │ ← sdist + wheel
└──────────────────────────────────┘Điểm mấu chốt: không có mã Python nào bị thực thi để đọc metadata. Toàn bộ [project] được parse như dữ liệu TOML tĩnh, loại bỏ hoàn toàn rủi ro bảo mật của setup.py.
Lịch sử PEP và quá trình chuẩn hóa
| PEP | Năm | Nội dung | Ý nghĩa |
|---|---|---|---|
| PEP 518 | 2016 | Định nghĩa [build-system] | Giải quyết chicken-and-egg |
| PEP 517 | 2017 | API chuẩn cho build backends | Thoát khỏi setuptools độc quyền |
| PEP 621 | 2021 | Chuẩn hóa [project] metadata | Metadata đồng nhất mọi backend |
| PEP 660 | 2021 | Editable installs qua PEP 517 | pip install -e . không cần setup.py |
| PEP 735 | 2024 | Dependency groups | Quản lý nhóm deps nâng cao |
Hiệu năng build backend
Benchmark: Build wheel — dự án 50 modules, 200 files
──────────────────────────────────────────────────────
flit_core ████░░░░░░░░░░░░ 0.8s (pure Python)
hatchling █████░░░░░░░░░░░ 1.2s (pure Python + metadata)
setuptools ██████████░░░░░░ 3.1s (full-featured)
maturin ██████████████░░ 4.5s (compile Rust)Bảng đánh đổi khi chọn backend
| Yếu tố | setuptools | hatchling | flit_core |
|---|---|---|---|
| Hệ sinh thái plugin | Rất phong phú | Đang phát triển | Tối thiểu |
| C/C++ extensions | ✅ Hỗ trợ đầy đủ | ❌ Không hỗ trợ | ❌ Không hỗ trợ |
| Đường cong học tập | Cao (nhiều option) | Trung bình | Thấp |
| Cấu hình mặc định | Cần nhiều | Thông minh | Gần như zero-config |
| Khả năng tùy biến | Rất cao | Cao | Thấp |
| Dùng khi | Legacy, C ext | Dự án mới, monorepo | Thư viện đơn giản |
Checklist ghi nhớ
✅ Checklist triển khai
Cấu trúc cơ bản
- [ ]
[build-system]có đầy đủrequiresvàbuild-backend - [ ]
[project]cóname,version(hoặcdynamic),requires-python - [ ]
dependenciesdùng khoảng version an toàn (>=X.Y,<Z.0) - [ ]
readmetrỏ đúng file README.md
Dependencies và entry points
- [ ]
[project.optional-dependencies]tách nhóm dev, docs, test - [ ]
[project.scripts]định nghĩa CLI entry points nếu cần - [ ] Environment markers cho dependencies đặc thù nền tảng
- [ ] Không pin cứng version cho thư viện, chỉ pin cho ứng dụng triển khai
Cấu hình công cụ
- [ ]
[tool.ruff],[tool.mypy],[tool.pytest.ini_options]thống nhất tham số - [ ]
line-lengthđồng nhất giữa formatter và linter - [ ]
[tool.setuptools.package-data]khai báo đủ file data cần đóng gói
Kiểm tra trước khi publish
- [ ]
python -m buildthành công không lỗi - [ ]
twine check dist/*không có cảnh báo - [ ] Cài thử wheel trên môi trường sạch và chạy test
Bài tập luyện tập
Bài 1: Viết pyproject.toml cho thư viện xử lý văn bản tiếng Việt
Bạn cần tạo pyproject.toml cho thư viện vn-text-processor với yêu cầu:
- Build backend: hatchling
- Python >= 3.10
- Dependencies:
underthesea>=6.8,<7.0vàregex>=2024.0 - Optional dev deps: pytest, ruff, mypy
- CLI entry point:
vntexttrỏ tớivn_text.cli:main - Source layout:
src/vn_text/
🧠 Quiz
Trường nào trong [project] là BẮT BUỘC theo PEP 621?
- [ ] A.
descriptionvàreadme - [x] B.
namevàversion(hoặc khai báodynamic = ["version"]) - [ ] C.
authorsvàlicense - [ ] D.
keywordsvàclassifiers
Giải thích: PEP 621 chỉ yêu cầu bắt buộc name. Tuy nhiên, version cũng bắt buộc trừ khi được liệt kê trong dynamic. Các trường còn lại đều tùy chọn nhưng nên có đầy đủ cho package chuyên nghiệp.
💡 Gợi ý
Bắt đầu với [build-system] cho hatchling, sau đó điền [project] metadata. Nhớ dùng [tool.hatch.build.targets.wheel] để chỉ định package trong src/.
✅ Lời giải
toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "vn-text-processor"
version = "0.1.0"
description = "Thư viện xử lý văn bản tiếng Việt"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Dev Team", email = "dev@example.com"},
]
dependencies = [
"underthesea>=6.8,<7.0",
"regex>=2024.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.5",
"mypy>=1.10",
]
[project.scripts]
vntext = "vn_text.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/vn_text"]Phân tích: Dùng hatchling làm backend vì dự án mới, pure Python, cấu hình gọn. Khoảng version >=X.Y,<Z.0 đảm bảo nhận patch nhưng không bị breaking change từ major bump.
Bài 2: Phát hiện và sửa lỗi trong pyproject.toml
File dưới đây có 4 lỗi. Tìm và sửa tất cả:
toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "data-pipeline"
version = "1.0.0"
dependencies = [
"pandas",
"sqlalchemy",
]
[project.optional-dependencies]
dev = ["pytest", "black"]
[tool.black]
line-length = 88
[tool.ruff]
line-length = 120🧠 Quiz
Có bao nhiêu lỗi trong file trên?
- [ ] A. 2 lỗi
- [ ] B. 3 lỗi
- [x] C. 4 lỗi
- [ ] D. 5 lỗi
Giải thích: (1) setuptools thiếu version tối thiểu, (2) thiếu requires-python, (3) dependencies không có version constraints, (4) line-length mâu thuẫn giữa black và ruff.
✅ Lời giải
toml
[build-system]
requires = ["setuptools>=61.0"] # Sửa 1: Pin version tối thiểu
build-backend = "setuptools.build_meta"
[project]
name = "data-pipeline"
version = "1.0.0"
requires-python = ">=3.10" # Sửa 2: Thêm requires-python
dependencies = [
"pandas>=2.0,<3.0", # Sửa 3: Thêm version constraints
"sqlalchemy>=2.0,<3.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "black>=24.0"]
[tool.black]
line-length = 88
[tool.ruff]
line-length = 88 # Sửa 4: Thống nhất line-lengthBài 3: Migration từ setup.py phức tạp
Chuyển đổi setup.py sau sang pyproject.toml sử dụng setuptools backend:
python
from setuptools import setup, find_packages
setup(
name="ml-serving",
version="2.3.1",
packages=find_packages(where="src"),
package_dir={"": "src"},
python_requires=">=3.11",
install_requires=[
"fastapi>=0.111",
"uvicorn[standard]>=0.30",
"torch>=2.3",
"numpy>=1.26",
],
extras_require={
"gpu": ["nvidia-cuda-runtime-cu12"],
"test": ["pytest", "httpx"],
},
entry_points={
"console_scripts": [
"ml-serve=ml_serving.main:cli",
],
},
package_data={"ml_serving": ["configs/*.yaml", "models/*.onnx"]},
)💡 Gợi ý
Ánh xạ từng trường: install_requires → dependencies, extras_require → [project.optional-dependencies], entry_points.console_scripts → [project.scripts], package_data → [tool.setuptools.package-data].
✅ Lời giải
toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "ml-serving"
version = "2.3.1"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.111,<1.0",
"uvicorn[standard]>=0.30,<1.0",
"torch>=2.3,<3.0",
"numpy>=1.26,<2.0",
]
[project.optional-dependencies]
gpu = ["nvidia-cuda-runtime-cu12"]
test = ["pytest>=8.0", "httpx>=0.27"]
[project.scripts]
ml-serve = "ml_serving.main:cli"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
ml_serving = ["configs/*.yaml", "models/*.onnx"]Phân tích: Lưu ý thêm upper bound cho mỗi dependency (<X.0) để bảo vệ khỏi breaking changes. Trường package_data chuyển thành [tool.setuptools.package-data] — đây là section riêng của setuptools backend, không thuộc PEP 621.