Giao diện
pyproject.toml & Dependencies Trung cấp
pyproject.toml = Tương lai của Python packaging = Một file để rule them all
Learning Outcomes
Sau khi hoàn thành trang này, bạn sẽ:
- ✅ Hiểu cấu trúc pyproject.toml và các sections quan trọng
- ✅ Migrate từ setup.py/setup.cfg sang pyproject.toml
- ✅ Quản lý dependencies với version constraints hiệu quả
- ✅ Hiểu lock files và tại sao chúng quan trọng
- ✅ So sánh build backends: setuptools, hatch, flit, maturin
Tại sao pyproject.toml?
The Old Way (setup.py Hell)
python
# setup.py - Dynamic, khó parse, security risks
from setuptools import setup, find_packages
setup(
name="my-package",
version="0.1.0",
packages=find_packages(),
install_requires=[
"requests>=2.25.0",
"numpy>=1.19.0",
],
extras_require={
"dev": ["pytest", "black"],
},
# ... 50+ more options
)Vấn đề với setup.py:
- ❌ Executable Python code → Security risks
- ❌ Khó parse bằng tools (phải execute để biết metadata)
- ❌ Không standardized format
- ❌ Chicken-and-egg: cần setuptools để đọc setup.py, nhưng setup.py define setuptools version
The New Way (pyproject.toml)
toml
# pyproject.toml - Static, declarative, standardized
[project]
name = "my-package"
version = "0.1.0"
dependencies = [
"requests>=2.25.0",
"numpy>=1.19.0",
]
[project.optional-dependencies]
dev = ["pytest", "black"]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"Ưu điểm:
- ✅ Static, declarative → Easy to parse
- ✅ Standardized (PEP 517, 518, 621)
- ✅ Tool-agnostic (works với setuptools, poetry, hatch, flit)
- ✅ Single source of truth cho project metadata
Cấu trúc pyproject.toml
Anatomy of pyproject.toml
toml
# ============================================
# BUILD SYSTEM (PEP 517/518)
# Defines how to build the package
# ============================================
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
# ============================================
# PROJECT METADATA (PEP 621)
# Core package information
# ============================================
[project]
name = "my-awesome-package"
version = "1.0.0"
description = "A short description of my package"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Your Name", email = "you@example.com"},
]
maintainers = [
{name = "Maintainer", email = "maintainer@example.com"},
]
keywords = ["python", "packaging", "example"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
# Dependencies
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0.0",
]
# Optional dependencies (extras)
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.0.0",
]
# Entry points
[project.scripts]
my-cli = "my_package.cli:main"
[project.gui-scripts]
my-gui = "my_package.gui:main"
[project.entry-points."my_package.plugins"]
plugin1 = "my_package.plugins:Plugin1"
# URLs
[project.urls]
Homepage = "https://github.com/username/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/username/my-package"
Changelog = "https://github.com/username/my-package/blob/main/CHANGELOG.md"
# ============================================
# TOOL CONFIGURATIONS
# Settings for various development tools
# ============================================
[tool.setuptools]
packages = ["my_package"]
package-dir = {"" = "src"}
[tool.setuptools.package-data]
my_package = ["py.typed", "data/*.json"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-ra -q --strict-markers"
[tool.black]
line-length = 88
target-version = ["py310", "py311", "py312"]
[tool.ruff]
line-length = 88
select = ["E", "F", "I", "N", "W", "UP"]
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = trueVersion Constraints
Syntax Overview
toml
[project]
dependencies = [
# Exact version (không khuyến khích cho libraries)
"package==1.2.3",
# Minimum version
"package>=1.2.3",
# Maximum version
"package<2.0.0",
# Range
"package>=1.2.3,<2.0.0",
# Compatible release (>=1.2.3, <2.0.0)
"package~=1.2.3",
# Compatible release (>=1.2, <2.0)
"package~=1.2",
# Wildcard (any 1.2.x)
"package==1.2.*",
# Exclude specific version
"package>=1.0.0,!=1.2.3",
# Multiple constraints
"package>=1.2.3,<2.0.0,!=1.5.0",
]Semantic Versioning (SemVer)
MAJOR.MINOR.PATCH
│ │ │
│ │ └── Bug fixes (backward compatible)
│ └──────── New features (backward compatible)
└────────────── Breaking changes (NOT backward compatible)
Examples:
1.0.0 → 1.0.1 # Bug fix
1.0.0 → 1.1.0 # New feature
1.0.0 → 2.0.0 # Breaking changeBest Practices cho Version Constraints
toml
# ❌ BAD: Quá strict (khó update)
dependencies = [
"requests==2.31.0",
"numpy==1.26.0",
]
# ❌ BAD: Quá loose (có thể break)
dependencies = [
"requests",
"numpy",
]
# ✅ GOOD: Flexible nhưng safe
dependencies = [
"requests>=2.28.0,<3.0.0",
"numpy>=1.24.0,<2.0.0",
]
# ✅ GOOD: Dùng compatible release operator
dependencies = [
"requests~=2.28", # >=2.28.0, <3.0.0
"numpy~=1.24", # >=1.24.0, <2.0.0
]Environment Markers
toml
[project]
dependencies = [
# Platform-specific
"pywin32>=300; sys_platform == 'win32'",
"pyobjc>=9.0; sys_platform == 'darwin'",
# Python version specific
"typing-extensions>=4.0; python_version < '3.11'",
"tomli>=2.0; python_version < '3.11'",
# Implementation specific
"cffi>=1.15; implementation_name == 'cpython'",
]Lock Files
Tại sao cần Lock Files?
toml
# pyproject.toml
dependencies = ["requests>=2.28.0"]
# Ngày 1: pip install → requests==2.28.0
# Ngày 30: pip install → requests==2.31.0 (new release)
# → Different versions = Different behavior = Bugs!Lock File = Snapshot của Dependencies
txt
# requirements.txt (lock file từ pip-tools)
#
# This file is autogenerated by pip-compile with Python 3.12
#
certifi==2023.11.17
# via requests
charset-normalizer==3.3.2
# via requests
idna==3.6
# via requests
requests==2.31.0
# via -r requirements.in
urllib3==2.1.0
# via requeststoml
# poetry.lock (excerpt)
[[package]]
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
python-versions = ">=3.7"
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"Lock File Workflow
bash
# === PIP-TOOLS ===
# 1. Define direct dependencies
echo "requests>=2.28.0" > requirements.in
# 2. Generate lock file
pip-compile requirements.in -o requirements.txt
# 3. Install from lock file
pip-sync requirements.txt
# 4. Update dependencies
pip-compile --upgrade requirements.in
# === POETRY ===
# 1. Add dependency (auto-updates poetry.lock)
poetry add requests
# 2. Install from lock file
poetry install
# 3. Update dependencies
poetry update
# === UV ===
# 1. Add dependency
uv add requests
# 2. Lock dependencies
uv lock
# 3. Install from lock file
uv sync
# 4. Update dependencies
uv lock --upgradeCommit Lock Files!
ini
# ❌ BAD: Ignore lock files
poetry.lock
requirements.txt
# ✅ GOOD: Commit lock files
# (Không có entry cho lock files)Build Backends
So sánh Build Backends
| Backend | Use Case | Speed | Features |
|---|---|---|---|
| setuptools | General, legacy | Medium | Full-featured |
| hatch | Modern projects | Fast | Environments, versioning |
| flit | Pure Python | Fast | Simple, minimal |
| maturin | Rust extensions | Fast | Rust + Python |
| poetry-core | Poetry projects | Medium | Poetry ecosystem |
1. setuptools (Default, Full-featured)
toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["my_package"]
package-dir = {"" = "src"}
[tool.setuptools.package-data]
my_package = ["py.typed", "data/*.json"]
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}Khi nào dùng:
- Legacy projects
- Cần C extensions
- Complex build requirements
2. hatch (Modern, Feature-rich)
toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/my_package/__about__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
[tool.hatch.envs.default]
dependencies = [
"pytest",
"pytest-cov",
]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
cov = "pytest --cov=my_package {args:tests}"bash
# Hatch commands
hatch new my-project # Create new project
hatch env create # Create environment
hatch run test # Run tests
hatch build # Build package
hatch publish # Publish to PyPI
hatch version minor # Bump versionKhi nào dùng:
- New projects
- Cần environment management
- Cần version management
3. flit (Simple, Pure Python)
toml
[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"
[project]
name = "my-package"
dynamic = ["version", "description"]
[tool.flit.module]
name = "my_package"bash
# Flit commands
flit init # Initialize project
flit build # Build package
flit publish # Publish to PyPI
flit install --symlink # Install in development modeKhi nào dùng:
- Pure Python packages
- Simple projects
- Minimal configuration
4. maturin (Rust Extensions)
toml
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "my-rust-package"
requires-python = ">=3.8"
[tool.maturin]
features = ["pyo3/extension-module"]
python-source = "python"
module-name = "my_rust_package._core"bash
# Maturin commands
maturin init # Initialize project
maturin develop # Build and install for development
maturin build # Build wheels
maturin publish # Publish to PyPIKhi nào dùng:
- Rust + Python projects
- Performance-critical code
- PyO3 bindings
Migration Guide: setup.py → pyproject.toml
Step 1: Analyze Current setup.py
python
# setup.py (before)
from setuptools import setup, find_packages
setup(
name="my-package",
version="1.0.0",
description="My awesome package",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Your Name",
author_email="you@example.com",
url="https://github.com/username/my-package",
packages=find_packages(where="src"),
package_dir={"": "src"},
python_requires=">=3.10",
install_requires=[
"requests>=2.28.0",
"pydantic>=2.0.0",
],
extras_require={
"dev": ["pytest", "black"],
},
entry_points={
"console_scripts": [
"my-cli=my_package.cli:main",
],
},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
],
)Step 2: Create pyproject.toml
toml
# pyproject.toml (after)
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "1.0.0"
description = "My awesome package"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Your Name", email = "you@example.com"},
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = ["pytest", "black"]
[project.scripts]
my-cli = "my_package.cli:main"
[project.urls]
Homepage = "https://github.com/username/my-package"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]Step 3: Remove Old Files
bash
# Xóa setup.py (hoặc giữ minimal version cho editable installs)
rm setup.py
# Xóa setup.cfg nếu có
rm setup.cfg
# Xóa MANIFEST.in nếu không cần
rm MANIFEST.inStep 4: Test Build
bash
# Build package
python -m build
# Check distribution
twine check dist/*
# Test install
pip install dist/my_package-1.0.0-py3-none-any.whlProduction Pitfalls ⚠️
1. Quên specify Python version
toml
# ❌ BAD: Không có requires-python
[project]
name = "my-package"
dependencies = ["pydantic>=2.0"] # Pydantic 2 cần Python 3.8+
# ✅ GOOD: Specify Python version
[project]
name = "my-package"
requires-python = ">=3.10"
dependencies = ["pydantic>=2.0"]2. Dùng dynamic version sai cách
toml
# ❌ BAD: Dynamic version từ file không tồn tại
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.version.__version__"} # File không có!
# ✅ GOOD: Đảm bảo file tồn tại
# src/my_package/__init__.py
# __version__ = "1.0.0"
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}3. Không include package data
toml
# ❌ BAD: Data files không được include
[tool.setuptools]
packages = ["my_package"]
# Missing package-data!
# ✅ GOOD: Include data files
[tool.setuptools.package-data]
my_package = [
"py.typed",
"data/*.json",
"templates/*.html",
]4. Conflicting tool configurations
toml
# ❌ BAD: Black và Ruff có line-length khác nhau
[tool.black]
line-length = 88
[tool.ruff]
line-length = 120 # Conflict!
# ✅ GOOD: Consistent configuration
[tool.black]
line-length = 88
[tool.ruff]
line-length = 885. Không test build trước khi publish
bash
# ❌ BAD: Publish trực tiếp
poetry publish
# ✅ GOOD: Test build trước
python -m build
twine check dist/*
pip install dist/*.whl
# Test package works
poetry publish # hoặc twine uploadQuick Reference
toml
# === MINIMAL pyproject.toml ===
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "1.0.0"
dependencies = ["requests>=2.28"]
# === VERSION CONSTRAINTS ===
"package==1.2.3" # Exact
"package>=1.2.3" # Minimum
"package<2.0.0" # Maximum
"package~=1.2" # Compatible (>=1.2, <2.0)
"package>=1.2,<2.0" # Range
# === COMMON TOOL CONFIGS ===
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.black]
line-length = 88
[tool.ruff]
select = ["E", "F", "I"]
[tool.mypy]
strict = trueCross-links
- Prerequisites: Virtual Environments - Environment isolation
- Next: CLI Tools (Click/Typer) - Build command-line applications
- Related: Distribution & Publishing - Publish to PyPI
- Related: Pytest Fundamentals - Testing configuration