| name | project-system |
| description | Guidance for Python project configuration and packaging using pyproject.toml, build backends, and modern PEP standards.
USE FOR: creating pyproject.toml, configuring build backends (setuptools, hatchling, flit-core, maturin, poetry-core, pdm-backend), entry points, version management, publishing to PyPI, editable installs, monorepo patterns
DO NOT USE FOR: installing packages or managing dependencies at runtime (use package-management), building CLI applications (use cli)
|
| license | MIT |
| metadata | {"displayName":"Python Project System","author":"Tyler-R-Kendrick","version":"1.0.0","tags":["python","pyproject","setuptools","hatch","flit","maturin","packaging","pep621"]} |
| compatibility | claude, copilot, cursor |
| references | [{"title":"Python Packaging User Guide - pyproject.toml","url":"https://packaging.python.org/en/latest/guides/writing-pyproject-toml/"},{"title":"PEP 621 - Storing Project Metadata in pyproject.toml","url":"https://peps.python.org/pep-0621/"},{"title":"Setuptools Documentation","url":"https://setuptools.pypa.io/en/latest/"}] |
Python Project System
Overview
Modern Python packaging has converged on pyproject.toml as the single source of truth for project metadata, build configuration, and tool settings. A series of PEPs have standardized how Python projects are built, distributed, and installed:
| PEP | Title | Impact |
|---|
| PEP 517 | Build system interface | Defines how frontends (pip, build) invoke backends (setuptools, hatchling) |
| PEP 518 | Build system requirements | Introduces [build-system] table in pyproject.toml |
| PEP 621 | Project metadata | Standardizes [project] table for name, version, dependencies, etc. |
| PEP 660 | Editable installs | Standardizes pip install -e . for PEP 517 backends |
| PEP 639 | License metadata | Introduces license-files and SPDX license expressions |
| PEP 723 | Inline script metadata | Allows single-file scripts to declare dependencies |
The key insight: the build backend is now pluggable. You choose a backend in [build-system], define metadata in [project], and any PEP 517-compatible frontend (pip, build, uv) can build your package.
pyproject.toml Anatomy
A complete, annotated pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "1.2.0"
description = "A short summary of the package"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
authors = [
{ name = "Jane Doe", email = "jane@example.com" },
]
maintainers = [
{ name = "Team Lead", email = "lead@example.com" },
]
keywords = ["automation", "tooling"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
,
,
,
,
,
]
= [
,
,
,
]
= [
,
,
,
,
]
= [
,
,
]
=
=
=
=
=
=
=
=
= []
= []
=
=
=
=
=
=
=
= [, , , , , , ]
= []
=
=
=
Build Backends Comparison
| Backend | Package | Strengths | Best For |
|---|
| setuptools | setuptools | Most mature, huge ecosystem, supports C extensions | Legacy projects, C extensions, maximum compatibility |
| hatchling | hatchling | Fast, modern, excellent defaults, Hatch project manager | New pure-Python projects, projects wanting a full workflow tool |
| flit-core | flit-core | Minimal and simple, very fast builds | Simple pure-Python packages with no special build steps |
| pdm-backend | pdm-backend | PEP 621 native, supports PEP 582 | Projects using pdm as their package manager |
| maturin | maturin | Builds Rust+Python (PyO3/cffi) packages | Rust extensions, high-performance compiled modules |
| poetry-core | poetry-core | Integrated with Poetry workflow | Projects already using Poetry (note: uses [tool.poetry] not [project] for some fields) |
Build System Configuration by Backend
setuptools (most common, most flexible):
[build-system]
requires = ["setuptools>=75.0", "wheel"]
build-backend = "setuptools.build_meta"
hatchling (recommended for new projects):
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
flit-core (minimal):
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"
pdm-backend:
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
maturin (Rust extensions):
[build-system]
requires = ["maturin>=1.7"]
build-backend = "maturin"
poetry-core:
[build-system]
requires = ["poetry-core>=1.9"]
build-backend = "poetry.core.masonry.api"
Source Layout vs Flat Layout
Source Layout (Recommended)
my-package/
pyproject.toml
README.md
LICENSE
src/
my_package/
__init__.py
core.py
utils.py
tests/
__init__.py
test_core.py
test_utils.py
Advantages:
- Prevents accidental imports of the development version (forces installation)
- Clear separation between source code and project metadata
- Avoids name collisions between the package directory and test/script imports
- Required by some backends (flit) by default
setuptools configuration for src layout:
[tool.setuptools.packages.find]
where = ["src"]
hatchling configuration for src layout:
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
Flat Layout
my-package/
pyproject.toml
README.md
LICENSE
my_package/
__init__.py
core.py
utils.py
tests/
test_core.py
Advantages:
- Simpler directory structure
- No
src/ prefix in import paths during development
- Works out of the box with most backends
setuptools configuration for flat layout:
[tool.setuptools.packages.find]
include = ["my_package*"]
Recommendation: Use the src layout for libraries and packages published to PyPI. Use the flat layout for applications and scripts that will not be distributed as packages.
Entry Points
Console Scripts
Console scripts create executable commands that are installed into the user's PATH:
[project.scripts]
my-cli = "my_package.cli:main"
my-tool = "my_package.tools:run"
The format is command-name = "module.path:function". The function is called with no arguments.
import sys
def main() -> int:
"""Entry point for the my-cli command."""
print("Hello from my-cli!")
return 0
if __name__ == "__main__":
sys.exit(main())
GUI Scripts
Same as console scripts but without opening a console window on Windows:
[project.gui-scripts]
my-gui = "my_package.gui:launch"
Plugin Entry Point Groups
Used for plugin systems where third-party packages can register extensions:
[project.entry-points."myapp.plugins"]
csv-export = "myapp_csv:CsvExporter"
json-export = "myapp_json:JsonExporter"
Discovering plugins at runtime:
from importlib.metadata import entry_points
def load_plugins():
eps = entry_points(group="myapp.plugins")
plugins = {}
for ep in eps:
plugins[ep.name] = ep.load()
return plugins
Version Management Strategies
Static Versioning
Define the version directly in pyproject.toml:
[project]
version = "1.2.0"
Update manually before each release. Simple but error-prone.
Dynamic Versioning from a Python File
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
__version__ = "1.2.0"
SCM-Based Versioning with setuptools-scm
Derive versions automatically from git tags:
[build-system]
requires = ["setuptools>=75.0", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
dynamic = ["version"]
[tool.setuptools_scm]
Workflow:
git tag v1.0.0
git push --tags
Hatch Version Management
[project]
dynamic = ["version"]
[tool.hatch.version]
path = "src/my_package/__about__.py"
__version__ = "1.2.0"
Bump with the CLI:
hatch version minor
hatch version patch
hatch version major
Package Discovery Configuration
setuptools Auto-Discovery
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.packages.find]
where = ["src"]
exclude = ["tests*"]
Including Data Files
[tool.setuptools.package-data]
my_package = ["data/*.json", "templates/*.html"]
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
[tool.hatch.build.targets.wheel.force-include]
"config/defaults.json" = "my_package/defaults.json"
Including or Excluding Files from SDist
[tool.hatch.build.targets.sdist]
include = ["src/", "tests/", "README.md", "LICENSE"]
exclude = ["*.pyc", "__pycache__"]
Building and Publishing Workflow
Building Distributions
Use the standard build tool (PEP 517 frontend):
pip install build
python -m build
Publishing to PyPI
Using twine (traditional):
pip install twine
twine upload --repository testpypi dist/*
twine upload dist/*
Using Trusted Publishers (recommended for CI/CD):
Trusted publishing eliminates API tokens by using OIDC identity from your CI provider. Configure on PyPI:
- Go to PyPI project settings and add a "trusted publisher"
- Configure your CI provider (GitHub Actions, GitLab CI, etc.)
- No secrets or tokens needed
GitHub Actions example:
name: Publish to PyPI
on:
release:
types: [published]
permissions:
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
environment: pypi
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
Using uv publish (modern alternative):
uv publish
uv publish --index-url https://test.pypi.org/legacy/
Editable Installs (PEP 660)
Editable installs let you develop a package without reinstalling after every change:
pip install -e .
pip install -e ".[dev,docs]"
uv pip install -e .
How it works: The backend creates a special .pth file or import hook that redirects imports to your source directory. Changes to source files take effect immediately.
Backend support:
- setuptools: Full support (uses import hooks or
.pth files)
- hatchling: Full support
- flit: Full support
- pdm-backend: Full support
- maturin: Supported (rebuilds Rust code on import with
maturin develop)
Monorepo Patterns
Workspace with Multiple Packages
monorepo/
pyproject.toml # Root project (optional, for workspace tools)
packages/
core/
pyproject.toml
src/core/
__init__.py
api/
pyproject.toml
src/api/
__init__.py
cli/
pyproject.toml
src/cli/
__init__.py
Inter-Package Dependencies
Reference sibling packages by path during development:
[project]
dependencies = [
"core",
]
uv Workspaces
uv supports native workspace management:
[tool.uv.workspace]
members = ["packages/*"]
uv sync
Hatch Workspaces
[tool.hatch.envs.default]
dependencies = [
"core @ {root:uri}/packages/core",
"api @ {root:uri}/packages/api",
]
Best Practices
-
Always use pyproject.toml. Do not create new setup.py or setup.cfg files. These are legacy.
-
Choose src layout for libraries. It prevents import confusion and is the community standard for published packages.
-
Pin your build backend version in [build-system].requires to avoid surprises:
requires = ["hatchling>=1.25,<2"]
-
Use requires-python to declare the minimum Python version:
requires-python = ">=3.11"
-
Specify dependency bounds. Use minimum versions with >= and optional upper bounds:
dependencies = [
"httpx>=0.27",
"pydantic>=2.0,<3",
]
-
Use optional dependency groups for dev, test, and docs dependencies to keep the core package lean.
-
Include a py.typed marker for typed packages:
src/my_package/py.typed # Empty file -- signals PEP 561 compliance
-
Use SCM-based versioning (setuptools-scm) for libraries to avoid manual version bumps.
-
Test your packaging before publishing:
python -m build
twine check dist/*
pip install dist/*.whl
-
Use trusted publishers for PyPI uploads from CI/CD instead of long-lived API tokens.