positive: "Use when user creates Python packages, asks about pyproject.toml, setup.cfg, setuptools, poetry, hatch, uv, flit, building wheels/sdists, publishing to PyPI, version management, or dependency specification."
Use pyproject.toml as the single source of truth. It replaces setup.py, setup.cfg, and MANIFEST.in for modern projects. Three top-level tables matter:
Declare the build backend. Always pin a minimum version.
[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"
All package metadata lives here. Keep it static when possible.
[project]
name = "my-package"
version = "1.2.0"
description = "Short description of what this does"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [{ name = "Your Name", email = "[email protected]" }]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"httpx>=0.27",
"pydantic>=2.0,<3",
]
[project.urls]
Homepage = "https://github.com/you/my-package"
Documentation = "https://my-package.readthedocs.io"
Consolidate tool config here instead of separate files:
[tool.ruff]
line-length = 88
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.mypy]
strict = true
| Backend | Best For | PEP 621 | C Extensions | Editable (PEP 660) |
|---|---|---|---|---|
| setuptools | Legacy, C extensions, max compat | Yes | Full support | Yes |
| hatchling | New projects, speed, plugins | Yes | Via hooks | Yes |
| flit | Simple pure-Python packages | Yes | No | Yes |
| poetry-core | Poetry users, all-in-one workflow | Yes (v2+) | Limited | Yes |
| maturin | Rust/Python hybrid (PyO3) | Yes | Rust only | Yes |
# setuptools
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
# hatchling
[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"
# flit
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"
# poetry
[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"
# maturin (Rust extensions)
[build-system]
requires = ["maturin>=1.7"]
build-backend = "maturin"
Choose hatchling for new pure-Python projects. Use setuptools when you need C/Cython extensions or legacy compatibility. Use maturin for Rust bindings.
| Feature | pip | uv | poetry | pdm | hatch |
|---|---|---|---|---|---|
| Written in | Python | Rust | Python | Python | Python |
| Speed | Moderate | 10-40× faster | Moderate | Fast | Fast |
| Lockfile | No (use pip-tools) | uv.lock | poetry.lock | pdm.lock | No |
| Venv management | Manual | Built-in | Built-in | Built-in | Built-in |
| Python version mgmt | No | Yes | No | Yes | No |
| Publishing | No (use twine) | Yes | Yes | Yes | Yes |
| Workspaces/monorepo | No | Yes | Experimental | Yes | Yes |
| Dependency groups (PEP 735) | Yes (pip 25.1+) | Yes | Yes | Yes | Yes |
Recommendations: Use uv for speed and CI/CD. Use poetry for teams wanting all-in-one workflow. Use hatch for plugin-driven development workflows.
my-project/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── core.py
│ └── py.typed
├── tests/
│ ├── __init__.py
│ └── test_core.py
├── pyproject.toml
├── README.md
└── LICENSE
Prevents accidental imports from the project root during testing. Catches packaging errors before publishing. Use for anything published to PyPI.
With setuptools, explicitly set package discovery:
[tool.setuptools.packages.find]
where = ["src"]
Hatchling and flit auto-detect src/ layout.
my-project/
├── my_package/
│ ├── __init__.py
│ └── core.py
├── tests/
├── pyproject.toml
└── README.md
Simpler but risks test pollution from local imports. Use for internal tools and applications not published to PyPI.
dependencies = [
"requests>=2.28,<3", # compatible range
"numpy>=1.24", # minimum only — use for stable APIs
"pydantic~=2.0", # equivalent to >=2.0,<3.0
"typing-extensions>=4.0;python_version<'3.12'", # environment marker
]
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.5", "mypy>=1.10"]
docs = ["sphinx>=7", "sphinx-rtd-theme"]
postgres = ["psycopg[binary]>=3.1"]
Install with pip install my-package[dev,docs] or uv pip install my-package[dev].
For development-only deps that should NOT be published as package extras:
[dependency-groups]
test = ["pytest>=8", "pytest-cov"]
lint = ["ruff>=0.5", "mypy>=1.10"]
dev = [
{ include-group = "test" },
{ include-group = "lint" },
]
Install with pip install --group test (pip 25.1+) or uv sync --group dev.
Dependency groups replace ad-hoc requirements-dev.txt files. Use extras for end-user-installable optional features. Use dependency groups for contributor/CI workflows.
[project.scripts]
my-cli = "my_package.cli:main"
Creates a my-cli executable on install that calls my_package.cli.main().
[project.gui-scripts]
my-app = "my_package.gui:launch"
Same as console scripts but suppresses console window on Windows.
[project.entry-points."my_package.plugins"]
csv = "my_package.plugins.csv:CsvPlugin"
json = "my_package.plugins.json:JsonPlugin"
Discover at runtime:
from importlib.metadata import entry_points
plugins = entry_points(group="my_package.plugins")
for ep in plugins:
plugin_class = ep.load()
[project]
version = "1.2.0"
Simple. Manually bump before each release.
[build-system]
requires = ["setuptools>=77", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
dynamic = ["version"]
[tool.setuptools_scm]
Derives version from git tags. Tag v1.2.0 → version 1.2.0. Untagged commits get dev versions like 1.2.1.dev3+g1a2b3c4.
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
dynamic = ["version"]
[tool.hatch.version]
source = "vcs"
Use bump2version or python-semantic-release for automated version bumps:
bump2version minor # 1.2.0 → 1.3.0
python -m build # builds both sdist and wheel in dist/
uv build # same, faster
hatch build # if using hatch
Always publish both sdist (.tar.gz) and wheel (.whl). Wheels skip the build step at install time.
pip install -e . # editable install for development
uv pip install -e . # same, faster
PEP 660 standardized editable installs for PEP 517 build backends. No setup.py needed. All modern backends (setuptools, hatchling, flit) support this.
python -m build
twine check dist/* # validate metadata
twine upload --repository testpypi dist/* # test first
twine upload dist/* # publish to PyPI
Configure on PyPI: Project Settings → Publishing → add GitHub Actions as trusted publisher. Specify owner, repo, workflow filename, and optionally a GitHub environment.
# .github/workflows/publish.yml