name: pyproject-modern-python description: Configure modern Python projects using pyproject.toml (PEP 621), hatchling build system with hatch-vcs for Git-based versioning, uv package manager with lockfile, optional dependencies and dependency-groups (PEP 735), and src-layout package structure. Use when setting up new Python projects, converting from setup.py, configuring CI for Python, or troubleshooting packaging issues. version: 1.0.0 tags: - python - packaging - pyproject - uv - hatchling - pep621
Modern Python Project Configuration
Overview
Configure Python projects using the modern pyproject.toml-centric approach with PEP 621 metadata, hatchling build system, hatch-vcs for Git-based versioning, uv package manager, and src-layout package structure.
When to Use
- Setting up a new Python project from scratch
- Converting legacy
setup.py/setup.cfgto modernpyproject.toml - Configuring CI/CD pipelines with uv
- Troubleshooting import errors in src-layout projects
- Adding optional dependencies or development dependency groups
- Implementing Git-based semantic versioning
Quick Reference
Minimal pyproject.toml
[project]
name = "my-package"
dynamic = ["version"]
description = "Package description"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"click>=8.0",
]
[project.scripts]
my-cli = "my_package.cli:main"
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
Directory Structure (src-layout)
my-project/
├── pyproject.toml
├── uv.lock
├── README.md
├── LICENSE
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── cli.py
└── tests/
├── __init__.py
└── test_cli.py
Complete Configuration Reference
Project Metadata (PEP 621)
Declare all project metadata in the [project] table:
[project]
name = "context-harness"
dynamic = ["version"]
description = "CLI installer for the ContextHarness agent framework"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "AGPL-3.0-or-later"}
authors = [
{name = "Your Name", email = "you@example.com"}
]
keywords = ["cli", "agents", "framework"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"click>=8.0",
"rich>=13.0",
"pyyaml>=6.0",
]
[project.urls]
Homepage = "https://github.com/org/project"
Repository = "https://github.com/org/project"
Issues = "https://github.com/org/project/issues"
Entry Points and Scripts
Define CLI commands in [project.scripts]:
[project.scripts]
my-cli = "my_package.cli:main"
For plugins or GUI scripts:
[project.gui-scripts]
my-gui = "my_package.gui:main"
[project.entry-points."myapp.plugins"]
plugin-name = "my_package.plugins:PluginClass"
Optional Dependencies
Use for features users can opt into:
[project.optional-dependencies]
# User installs: pip install my-package[keyring]
keyring = ["keyring>=24.0"]
# Multiple feature groups
aws = ["boto3>=1.26"]
all = ["keyring>=24.0", "boto3>=1.26"]
Development Dependencies (PEP 735)
Use [dependency-groups] for development tools (not shipped to users):
[dependency-groups]
dev = [
"pytest>=8.0",
"pytest-cov>=4.0",
]
lint = [
"ruff>=0.1.0",
"mypy>=1.0",
]
docs = [
"sphinx>=7.0",
]
Install with: uv sync --group dev or uv sync --all-groups
Build System Configuration
Configure hatchling with hatch-vcs for Git-based versioning:
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
[tool.hatch.version.raw-options]
fallback_version = "0.0.0+unknown"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
[tool.hatch.build.targets.sdist]
include = [
"src/",
"README.md",
"LICENSE",
]
Dynamic Version Access
Access version at runtime using importlib.metadata:
"""my_package/__init__.py"""
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("my-package")
except PackageNotFoundError:
# Running from source without installation
__version__ = "0.0.0+unknown"
Pytest Configuration
Configure pytest for src-layout projects:
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
Critical: The pythonpath = ["src"] setting enables imports like from my_package import ... in tests without installing the package.
CI/CD with uv
GitHub Actions Workflow
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- name: Run tests
run: uv run pytest tests/ -v
- name: Verify CLI works
run: uv run my-cli --help
Semantic Release Workflow
For automated versioning based on conventional commits:
name: Release
on:
push:
branches: [main]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- run: npm clean-install
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
Semantic Versioning with hatch-vcs
How It Works
- hatch-vcs reads Git tags to determine version
- semantic-release creates tags based on commit messages
- Version format:
X.Y.Zfrom tags, orX.Y.Z.devN+gHASHfor commits after tag
Tag-Based Versioning
| Git State | Version Output |
|---|---|
On tag v1.2.3 |
1.2.3 |
5 commits after v1.2.3 |
1.2.4.dev5+g1234abc |
| No tags in repo | 0.0.0+unknown (fallback) |
| Dirty working tree | 1.2.3+d20231215 |
Conventional Commits for Version Bumps
| Commit Prefix | Version Bump | Example |
|---|---|---|
fix: |
PATCH (0.0.X) | fix: resolve import error |
feat: |
MINOR (0.X.0) | feat: add new command |
feat!: or BREAKING CHANGE: |
MAJOR (X.0.0) | feat!: redesign API |
Migration from setup.py
Step-by-Step Migration
- Create pyproject.toml with project metadata
- Move dependencies from
install_requiresto[project]dependencies - Move extras from
extras_requireto[project.optional-dependencies] - Move entry points from
entry_pointsto[project.scripts] - Configure build system with hatchling
- Delete
setup.py,setup.cfg,MANIFEST.in
Translation Table
| setup.py / setup.cfg | pyproject.toml |
|---|---|
name="pkg" |
[project] name = "pkg" |
version="1.0.0" |
dynamic = ["version"] + hatch-vcs |
install_requires=[...] |
dependencies = [...] |
extras_require={...} |
[project.optional-dependencies] |
entry_points={...} |
[project.scripts] |
python_requires=">=3.9" |
requires-python = ">=3.9" |
packages=find_packages() |
[tool.hatch.build.targets.wheel] |
Example Migration
Before (setup.py):
setup(
name="my-package",
version="1.0.0",
packages=find_packages(where="src"),
package_dir={"": "src"},
install_requires=["click>=8.0"],
extras_require={"dev": ["pytest"]},
entry_points={"console_scripts": ["mycli=my_package.cli:main"]},
)
After (pyproject.toml):
[project]
name = "my-package"
dynamic = ["version"]
dependencies = ["click>=8.0"]
[project.scripts]
mycli = "my_package.cli:main"
[dependency-groups]
dev = ["pytest>=8.0"]
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
Troubleshooting
Import Errors
Problem: ModuleNotFoundError: No module named 'my_package'
Causes and Solutions:
| Cause | Solution |
|---|---|
| Package not installed | Run uv sync or uv pip install -e . |
Missing pythonpath in pytest |
Add pythonpath = ["src"] to [tool.pytest.ini_options] |
| Wrong package path in wheel config | Verify packages = ["src/my_package"] matches actual structure |
__init__.py missing |
Add __init__.py to all package directories |
Debug Command:
uv run python -c "import my_package; print(my_package.__file__)"
Version Shows "0.0.0+unknown"
Problem: Version always returns fallback value
Causes and Solutions:
| Cause | Solution |
|---|---|
| No Git tags | Create initial tag: git tag v0.1.0 |
| Not a Git repo | Initialize: git init && git add . && git commit -m "init" |
| Package not installed | Run uv sync to install in editable mode |
| Shallow clone in CI | Use fetch-depth: 0 in checkout action |
uv.lock Conflicts
Problem: Lockfile conflicts after merge
Solution:
# Regenerate lockfile
rm uv.lock
uv lock
# Or resolve specific package
uv lock --upgrade-package problematic-package
Build Fails with hatch-vcs
Problem: hatch-vcs cannot determine version
Checklist:
- Is
.gitdirectory present? - Does
git describe --tagsreturn a version? - Is
hatch-vcsinbuild-system.requires? - Is
[tool.hatch.version] source = "vcs"configured?
Fallback for development:
[tool.hatch.version.raw-options]
fallback_version = "0.0.0+unknown"
Optional Dependency Not Found
Problem: Import fails for optional dependency feature
Solution: Wrap imports with try/except:
try:
import keyring
HAS_KEYRING = True
except ImportError:
HAS_KEYRING = False
keyring = None
def store_token(token: str) -> None:
if not HAS_KEYRING:
raise RuntimeError("Install with: pip install my-package[keyring]")
keyring.set_password("service", "user", token)
Common uv Commands
| Command | Purpose |
|---|---|
uv init |
Create new project with pyproject.toml |
uv sync |
Install dependencies from lockfile |
uv sync --group dev |
Include dev dependency group |
uv sync --all-groups |
Include all dependency groups |
uv add click |
Add dependency to project |
uv add --dev pytest |
Add to dev dependency group |
uv lock |
Update lockfile |
uv run pytest |
Run command in project environment |
uv build |
Build wheel and sdist |
uv publish |
Publish to PyPI |
Best Practices
- Always use src-layout - Prevents accidental imports from source directory
- Lock dependencies - Commit
uv.lockfor reproducible builds - Use dynamic versioning - Let Git tags drive version numbers
- Separate dev dependencies - Use
[dependency-groups]not[project.optional-dependencies] - Configure pytest pythonpath - Essential for src-layout test imports
- Set fallback version - Prevents build failures in edge cases
- Use conventional commits - Enables automated semantic versioning
- Fetch full history in CI - Required for hatch-vcs to compute versions
Related Standards
- PEP 621 - Project metadata in pyproject.toml
- PEP 735 - Dependency groups
- PEP 517 - Build system interface
- PEP 518 - Build system requirements
Skill: pyproject-modern-python v1.0.0 | Last updated: 2025-12-31