Skip to content

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ấtpyproject.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ập

Bả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 config

Cố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 = true

So sánh Build Backends

Tiêu chísetuptoolshatchlingflit_corematurin
Trường hợp dùngĐa năng, legacyDự án hiện đạiPure Python đơn giảnRust extensions
Tốc độ buildTrung bìnhNhanhNhanhNhanh
C extensions✅ (Rust)
Quản lý versionThủ công/dynamicTích hợp sẵnTừ __init__.pyTừ 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 = true

Migration: 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-dir

Bướ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

PEPNămNội dungÝ nghĩa
PEP 5182016Định nghĩa [build-system]Giải quyết chicken-and-egg
PEP 5172017API chuẩn cho build backendsThoát khỏi setuptools độc quyền
PEP 6212021Chuẩn hóa [project] metadataMetadata đồng nhất mọi backend
PEP 6602021Editable installs qua PEP 517pip install -e . không cần setup.py
PEP 7352024Dependency groupsQuả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ốsetuptoolshatchlingflit_core
Hệ sinh thái pluginRất phong phúĐang phát triểnTối thiểu
C/C++ extensions✅ Hỗ trợ đầy đủ❌ Không hỗ trợ❌ Không hỗ trợ
Đường cong học tậpCao (nhiều option)Trung bìnhThấp
Cấu hình mặc địnhCần nhiềuThông minhGần như zero-config
Khả năng tùy biếnRất caoCaoThấp
Dùng khiLegacy, C extDự án mới, monorepoThư viện đơn giản

Checklist ghi nhớ

✅ Checklist triển khai

Cấu trúc cơ bản

  • [ ] [build-system] có đầy đủ requiresbuild-backend
  • [ ] [project]name, version (hoặc dynamic), requires-python
  • [ ] dependencies dùng khoảng version an toàn (>=X.Y,<Z.0)
  • [ ] readme trỏ đú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 build thà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.0regex>=2024.0
  • Optional dev deps: pytest, ruff, mypy
  • CLI entry point: vntext trỏ tới vn_text.cli:main
  • Source layout: src/vn_text/

🧠 Quiz

Trường nào trong [project] là BẮT BUỘC theo PEP 621?

  • [ ] A. descriptionreadme
  • [x] B. nameversion (hoặc khai báo dynamic = ["version"])
  • [ ] C. authorslicense
  • [ ] D. keywordsclassifiers

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-length

Bà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_requiresdependencies, 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.


Liên kết học tiếp