name: modern-python-packaging description: Configures modern Python packaging with uv, hatchling, pyproject.toml, and dependency resolution for reproducible builds in 2025+. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: python packaging, uv tool, pyproject.toml, hatchling build backend, rye project manager, dependency resolution, Python publish workflow, how do i package a Python project archetypes: - tactical - generation anti_triggers: - brainstorming - vague ideation response_profile: verbosity: low directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code content-types: [code, guidance, config, examples] related-skills: coding-modern-python-development, coding-testing-patterns, coding-cve-dependency-management
Modern Python Packaging
Build configuration engineer configuring modern Python packaging tooling for reproducible, fast builds and distribution. This skill makes the model act as a packaging specialist — selecting build backends, writing complete pyproject.toml configurations, managing dependency resolution with uv or rye, and preparing packages for publication on PyPI or private indexes.
TL;DR Checklist
- Choose hatchling as the default build backend unless project requirements dictate otherwise
- Write a complete pyproject.toml following PEP 621 — no setup.py, no setup.cfg
- Pin
requires-pythonto a minimum version (e.g.,>=3.12) - Commit the lockfile (
uv.lock) for deterministic builds across environments - Use
uv add/uv syncfor all dependency management — never pip install manually in project workflows - Configure pytest with explicit discovery root and coverage thresholds
- Version bump using a script or tool that reads/writes pyproject.toml atomically
When to Use
Use this skill when:
- Setting up a new Python project from scratch with modern tooling
- Migrating an existing project from setuptools/setup.py to pyproject.toml
- Configuring dependency resolution for reproducible CI/CD builds
- Preparing a package for publication on PyPI or a private index
- Choosing between build backends (hatchling, flit, pdm, setuptools) for a specific project type
- Resolving dependency conflicts using uv's resolver
When NOT to Use
Avoid this skill for:
- Legacy projects that must support Python 3.9 or earlier with older tooling chains
- Projects committed to Poetry as their primary package manager — Poetry has its own ecosystem
- Simple scripts that don't need packaging (no distribution, no installable entry points)
- Virtual environment management for non-project workflows — use
uv venvdirectly instead
Core Workflow
Step 1: Choose Build Backend Based on Project Requirements
Select the build backend by matching project needs to each tool's strengths:
| Requirement | Recommended Backend | Why |
|---|---|---|
| Standard library, broad compatibility | hatchling | Fast, stable, PEP 621 native, best uv integration |
| Minimal config, no plugins needed | flit | Simplest setup, but fewer features |
| Complex build steps (C extensions, code gen) | setuptools + build |
Mature ecosystem, but legacy format |
| PDM-native project using lockfile exclusively | pdm-backend | Tight integration with PDM workflow |
Checkpoint: Backend selected and justified. If choosing hatchling (recommended default), verify no C extension builds are required — if yes, use setuptools.
def select_build_backend(
project_type: str, # "library", "cli-tool", "data-science", "web-service"
has_c_extensions: bool = False,
needs_code_generation: bool = False,
) -> dict:
"""Recommend a build backend based on project characteristics.
Args:
project_type: The kind of Python project being built
has_c_extensions: Whether the project compiles C/Cython extensions
needs_code_generation: Whether build-time code generation is required
Returns:
Dict with recommended backend and justification
"""
if has_c_extensions or needs_code_generation:
return {
"backend": "setuptools",
"requires": ["setuptools>=75.0", "wheel"],
"build_backend": "setuptools.build_meta",
"justification": (
f"C extensions or code generation require setuptools build_meta. "
f"For project_type='{project_type}', consider using setuptools with "
f"pyproject.toml metadata while keeping setup.py only for build hooks."
),
}
if project_type == "library":
return {
"backend": "hatchling",
"requires": ["hatchling"],
"build_backend": "hatchling.build",
"justification": (
f"hatchling is the recommended backend for Python libraries. "
f"It provides fast builds, native PEP 621 support, plugin extensibility, "
f"and excellent uv integration. Best choice for project_type='{project_type}'."
),
}
if project_type == "cli-tool":
return {
"backend": "hatchling",
"requires": ["hatchling"],
"build_backend": "hatchling.build",
"justification": (
f"hatchling supports entry point definition via [project.scripts] or "
f"[project.gui-scripts]. Ideal for CLI tools that need console_scripts "
f"entry points. Best choice for project_type='{project_type}'."
),
}
if project_type == "data-science":
return {
"backend": "hatchling",
"requires": ["hatchling"],
"build_backend": "hatchling.build",
"justification": (
f"hatchling with [tool.hatch.build.targets.wheel] configuration "
f"supports data files, package data inclusion, and selective file "
f"exclusion needed for data science projects. Best choice for project_type='{project_type}'."
),
}
return {
"backend": "hatchling",
"requires": ["hatchling"],
"build_backend": "hatchling.build",
"justification": f"Defaulting to hatchling for project_type='{project_type}'.",
}
Step 2: Write Complete pyproject.toml Configuration
Step 2: Create a PEP 621 compliant pyproject.toml with all required sections.
This is the single source of truth — no setup.py, no setup.cfg, no requirements.txt.
# ❌ BAD — legacy setuptools approach; multiple config files scattered across project
[metadata]
name = my-python-library
version = 0.1.0
description = A library that does something useful
author = First Last
author_email = author@example.com
[options]
packages = find:
python_requires = >=3.8
install_requires =
httpx>=0.28
pydantic>=2.10
[options.extras_require]
dev = pytest; ruff
# Requires a separate setup.py for build hooks, separate requirements.txt for docs
# ❌ BAD: no lockfile — nondeterministic builds
# ❌ BAD: Python 3.8 is end-of-life, broad range causes resolver conflicts
# ✅ GOOD — Complete pyproject.toml for a library project (hatchling backend)
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "my-python-library"
dynamic = ["version"]
description = "A concise one-line description of what this library does"
readme = "README.md"
requires-python = ">=3.12"
license = {text = "MIT"}
authors = [
{name = "First Last", email = "author@example.com"},
]
keywords = ["keyword1", "keyword2", "keyword3"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"httpx>=0.28,<1.0",
"pydantic>=2.10,<3.0",
"structlog>=24.1",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3",
"pytest-asyncio>=0.25",
"pytest-cov>=6.0",
"ruff>=0.9",
"mypy>=1.14",
]
docs = [
"mkdocs>=1.6",
"mkdocstrings[python]>=0.27",
]
[project.scripts]
my-cli = "my_library.cli:main"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.targets.wheel]
packages = ["src/my_library"]
[tool.ruff]
target-version = "py312"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "PLC", "PLE"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
filterwarnings = [
"error",
"ignore::DeprecationWarning",
]
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.coverage.run]
source = ["my_library"]
branch = true
[tool.coverage.report]
fail_under = 80
show_missing = true
# ✅ For a CLI tool project, add entry points and GUI scripts if applicable
# [project.scripts]
# my-tool = "my_tool.cli:main"
#
# [project.gui-scripts]
# gui-tool = "my_tool.gui:main"
Checkpoint: pyproject.toml is complete with all sections. Verify that packages in [tool.hatch.build.targets.wheel] matches the actual source directory structure.
Step 3: Configure Dependency Resolution with uv
Step 3: Set up deterministic dependency resolution and lock management.
# Initialize a new project with default pyproject.toml (hatchling + src layout)
uv init my-project --name my-project
cd my-project
# Add production dependencies (resolves versions, updates uv.lock instantly)
uv add httpx pydantic structlog
# Add optional dependency groups
uv add --optional dev pytest pytest-asyncio pytest-cov ruff mypy
uv add --optional docs mkdocs mkdocstrings[python]
# Pin exact version for a specific dependency (useful for critical dependencies)
uv add "pydantic>=2.10,<3.0"
# Run tooling inside the project environment — no manual venv activation
uv run python -m my_project.cli --help
uv run pytest
uv run ruff check .
uv run mypy src/
# Sync dependencies from lockfile (deterministic, fast)
uv sync
# Update all dependencies to newest compatible versions
uv lock --upgrade
# Update a single dependency
uv lock --upgrade-package httpx
# Add a development-only dependency that doesn't affect production installs
uv add --dev black isort
# Remove a dependency
uv remove structlog
Checkpoint: uv.lock file exists and is committed. Run uv sync --locked in CI to enforce lockfile determinism — this fails if the lockfile is out of sync with pyproject.toml.
def verify_lockfile_integrity(project_dir: str) -> dict:
"""Verify that uv.lock matches pyproject.toml dependencies.
This is what should run in CI to prevent undetected dependency drift.
Args:
project_dir: Path to the project root containing pyproject.toml and uv.lock
Returns:
Dict with verification result and any discrepancies found
"""
import subprocess
try:
# uv sync --locked fails if lockfile doesn't match pyproject.toml
result = subprocess.run(
["uv", "sync", "--locked"],
cwd=project_dir,
capture_output=True,
text=True,
timeout=120,
)
return {
"integrity_ok": result.returncode == 0,
"output": result.stdout + result.stderr,
"discrepancies": [] if result.returncode == 0 else _parse_discrepancies(result.stderr),
}
except subprocess.TimeoutExpired:
return {
"integrity_ok": False,
"output": "uv sync timed out after 120 seconds",
"discrepancies": ["dependency resolution timeout"],
}
def _parse_discrepancies(stderr: str) -> list[str]:
"""Parse uv sync error output for human-readable discrepancy descriptions."""
issues = []
if "mismatch" in stderr.lower():
issues.append("Lockfile does not match pyproject.toml dependencies")
if "not found" in stderr.lower() or "could not find" in stderr.lower():
issues.append("A required package could not be resolved from the index")
if "version" in stderr.lower() and ("conflict" in stderr.lower() or "incompatible" in stderr.lower()):
issues.append("Version conflict between dependency requirements")
return issues if issues else [stderr.strip()[:200]]
Step 4: Set Up Local Development Environment
Step 4: Configure the development environment for a frictionless developer experience.
# Create and activate the project environment (automatic, no manual venv commands)
uv sync # Installs all dependencies from uv.lock into project venv
# Run any command with correct environment — no source .venv/bin/activate needed
uv run pytest tests/
uv run ruff check src/ --fix
uv run mypy src/my_library/
# Add packages interactively (with dependency resolution preview)
uv add package-name
# Quick one-off script execution in project context
uv run python -c "import my_library; print(my_library.__version__)"
# Run pre-commit hooks before committing (if configured)
uv run pre-commit run --all-files
# For a data science project with heavy dependencies, use workspace mode
mkdir my-workspace && cd my-workspace
uv venv --python 3.12 # Create shared workspace venv
uv pip install numpy pandas scikit-learn
Checkpoint: uv run works for all tooling commands without manual virtual environment activation. If uv run pytest fails, check that the test paths in pyproject.toml match the actual directory structure.
Step 5: Configure Testing Integration
Step 5: Integrate testing and coverage into the build configuration.
# ✅ GOOD — pytest + coverage configuration in pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
filterwarnings = [
"error", # Treat all warnings as errors
"ignore::DeprecationWarning:pkg_resources", # Ignore known third-party deprecations
"ignore::DeprecationWarning:yaml",
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests that require external services",
"network: marks tests that make network calls",
]
[tool.coverage.run]
source = ["my_library"] # Measure coverage only for production code
branch = true # Track branch coverage, not just line coverage
omit = [
"tests/**", # Exclude test files
"src/my_library/cli.py", # Exclude CLI entry points from coverage
"**/__about__.py", # Exclude version stubs
]
[tool.coverage.report]
fail_under = 80 # CI fails if coverage drops below 80%
show_missing = true # Show which lines are missing coverage
skip_empty = true # Skip files with no statements
sort = "Cover" # Sort report by coverage percentage
# ✅ GOOD — ruff integration for linting and formatting
[tool.ruff]
target-version = "py312"
line-length = 100
show-fixes = true
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes (unused imports, undefined names)
"I", # isort (import sorting)
"UP", # pyupgrade (modernize syntax)
"B", # flake8-bugbear (common bugs)
"SIM", # flake8-simplify (simplify expressions)
"RUF", # ruff-specific rules
"PLC", # pylint conventions
"PLE", # pylint errors
]
ignore = [
"E501", # Line length handled by formatter
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"] # Allow assert in tests (flake8-bugbear)
Checkpoint: uv run ruff check . passes with no errors. Coverage threshold set to at least 80% — adjust based on project maturity.
Step 6: Prepare for Publication
Step 6: Configure versioning, changelog management, and pre-publish validation.
"""Version bumping automation that reads/writes pyproject.toml."""
def bump_version(
project_dir: str = ".",
bump_type: str = "patch",
) -> dict:
"""Bump the version in pyproject.toml using hatch-vcs or manual version field.
This function works with both dynamic (vcs-driven) and static versions.
Args:
project_dir: Path to project root containing pyproject.toml
bump_type: One of 'patch', 'minor', 'major' — ignored for vcs-driven versions
Returns:
Dict with previous version, new version, and bump metadata
"""
import subprocess
import tomllib
from pathlib import Path
pyproject_path = Path(project_dir) / "pyproject.toml"
# Read current configuration
with open(pyproject_path, "rb") as f:
config = tomllib.load(f)
is_dynamic_version = "dynamic" in config.get("project", {}) and "version" in config["project"]["dynamic"]
if is_dynamic_version:
# For hatch-vcs managed versions, trigger a release commit/tag
result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"],
cwd=project_dir,
capture_output=True,
text=True,
)
if result.returncode != 0:
return {
"success": False,
"error": "No git tags found — cannot compute version from VCS",
}
# hatch-vcs will compute version from the latest tag + commit count
new_version_tag = _compute_next_tag(result.stdout.strip(), bump_type)
return {
"success": True,
"method": "vcs",
"previous_tag": result.stdout.strip(),
"new_tag": new_version_tag,
"note": "Run 'git tag' and 'git push --tags' to publish this version",
}
else:
# Static version — read directly from pyproject.toml
current_version = config.get("project", {}).get("version", "0.0.0")
new_version = _bump_static_version(current_version, bump_type)
# Update pyproject.toml with new version (atomic write)
content = pyproject_path.read_text()
import re
updated = re.sub(
r'^(version\s*=\s*)".*?"$',
f'\\1"{new_version}"',
content,
flags=re.MULTILINE,
)
pyproject_path.write_text(updated)
return {
"success": True,
"method": "static",
"previous_version": current_version,
"new_version": new_version,
"note": f"Updated pyproject.toml version from {current_version} to {new_version}",
}
def _compute_next_tag(latest_tag: str, bump_type: str) -> str:
"""Compute the next git tag based on semver bump type."""
# Strip 'v' prefix if present
tag_base = latest_tag.lstrip("v")
major, minor, patch = map(int, tag_base.split("."))
if bump_type == "major":
major += 1
minor = 0
patch = 0
elif bump_type == "minor":
minor += 1
patch = 0
else: # patch (default)
patch += 1
return f"v{major}.{minor}.{patch}"
def _bump_static_version(current: str, bump_type: str) -> str:
"""Bump a static semver version string."""
major, minor, patch = map(int, current.split("."))
if bump_type == "major":
return f"{major + 1}.0.0"
elif bump_type == "minor":
return f"{major}.{minor + 1}.0"
else:
return f"{major}.{minor}.{patch + 1}"
def pre_publish_check(project_dir: str = ".") -> list[str]:
"""Run all pre-publish checks before building a distribution.
Returns a list of errors found (empty list means ready to publish).
Checks performed:
1. Version is not a development/pre-release version
2. README.md exists and renders correctly
3. All dependencies have pinned versions compatible with requires-python
4. Build artifacts from previous releases are cleaned
"""
import subprocess
from pathlib import Path
errors = []
pyproject_path = Path(project_dir) / "pyproject.toml"
# Check 1: Version is not pre-release
try:
with open(pyproject_path, "rb") as f:
config = __import__("tomllib").load(f)
version = config.get("project", {}).get("version", "")
if version and any(part in version for part in ("alpha", "beta", "rc", ".dev")):
errors.append(
f"Version '{version}' is a pre-release — bump to stable version before publishing"
)
except Exception as e:
errors.append(f"Failed to read pyproject.toml: {e}")
# Check 2: README exists
readme_files = ["README.md", "README.rst", "README.txt"]
if not any((Path(project_dir) / rf).exists() for rf in readme_files):
errors.append("No README found — PyPI requires a long_description")
# Check 3: Clean build artifacts
build_dirs = [Path(project_dir) / "dist", Path(project_dir) / "build"]
for bd in build_dirs:
if bd.exists() and any(bd.iterdir()):
errors.append(
f"Build directory '{bd}' exists and is not empty — run 'rm -rf dist build' first"
)
# Check 4: Build test
result = subprocess.run(
["uv", "build", "--sdist", "--wheel"],
cwd=project_dir,
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
errors.append(f"Build failed: {result.stderr[:300]}")
return errors
Checkpoint: pre_publish_check() returns an empty list. If not, fix each error before running uv publish.
Implementation Patterns
Pattern 1: Complete pyproject.toml for Different Project Types
# ============================================================================
# Library project (src layout) — most common case
# ============================================================================
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-library"
version = "0.1.0"
description = "A library that does something useful"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28",
"pydantic>=2.10",
]
[project.optional-dependencies]
dev = ["pytest>=8.3", "ruff>=0.9"]
[tool.hatch.build.targets.wheel]
packages = ["src/my_library"]
# ============================================================================
# CLI tool project — includes entry point scripts
# ============================================================================
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-cli-tool"
version = "1.0.0"
description = "A command-line tool for doing things"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"click>=8.1",
"rich>=13.0",
]
[project.scripts]
my-cli = "my_cli_tool.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/my_cli_tool"]
# ============================================================================
# Data science project — includes data files in the package
# ============================================================================
[build-system]
requires = ["hatchling", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"
[project]
name = "my-data-project"
version = "0.5.0"
description = "A data science package with model artifacts"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"numpy>=1.26",
"pandas>=2.2",
"scikit-learn>=1.5",
]
# Include data files (CSV, JSON, model artifacts) in the wheel
[tool.hatch.build.targets.wheel]
packages = ["src/my_data_project"]
artifacts = [
"src/my_data_project/data/*.csv",
"src/my_data_project/data/*.json",
"src/my_data_project/models/*.pkl",
]
# Exclude unnecessary files (reduces package size)
[tool.hatch.build.targets.wheel.only-packages]
include = ["my_data_project"]
Pattern 2: uv Dependency Management Workflow Reference
# ── Initialization ───────────────────────────────────────────────
uv init my-project # Scaffold new project with pyproject.toml
cd my-project
uv sync # Create .venv and install deps from lockfile
# ── Adding Dependencies ──────────────────────────────────────────
uv add httpx # Add to production dependencies
uv add --dev pytest # Add to dev dependencies only
uv add "package>=1.0,<2.0" # With version constraints
# ── Managing Lockfile ───────────────────────────────────────────
uv sync # Sync from lockfile (fast, deterministic)
uv lock # Update lockfile based on current pyproject.toml
uv lock --upgrade # Upgrade all packages to newest compatible versions
uv lock --upgrade-package httpx # Upgrade just one package
uv sync --locked # CI mode: fail if lockfile drifts from pyproject.toml
# ── Building and Publishing ─────────────────────────────────────
uv build # Build sdist and wheel
uv publish # Publish to PyPI (requires API token)
# Alternative: manually upload
twine upload dist/* # Traditional approach if not using uv publish
# ── Workspace Mode (multi-package projects) ────────────────────
uv new workspace-parent --lib
cd workspace-parent
uv x add package1 package2 # Add to workspace root
uv sync # Installs all workspace packages
Pattern 3: Version Bump Automation with Git Integration
"""Complete version bump tool with git tag and changelog management."""
import subprocess
from pathlib import Path
def release_workflow(
project_dir: str = ".",
bump_type: str = "patch",
skip_publish: bool = False,
skip_tests: bool = False,
) -> dict:
"""Execute a complete release workflow: bump version, run tests, build, publish.
Args:
project_dir: Project root path
bump_type: 'major', 'minor', or 'patch'
skip_publish: If True, stop after building (dry-run mode)
skip_tests: If True, skip test verification before release
Returns:
Dict with step results and final version
"""
steps = []
errors = []
# Step 1: Pre-publish checks
try:
from .version import pre_publish_check # from Step 6 above
check_results = pre_publish_check(project_dir)
if check_results:
return {
"success": False,
"error": "Pre-publish checks failed",
"steps": [{"step": "pre_publish_check", "errors": check_results}],
}
steps.append({"step": "pre_publish_check", "status": "passed"})
except ImportError:
steps.append({"step": "pre_publish_check", "status": "skipped (not available)"})
# Step 2: Run tests if not skipped
if not skip_tests:
result = subprocess.run(
["uv", "run", "pytest", "-x", "--tb=short"],
cwd=project_dir,
capture_output=True,
text=True,
timeout=300,
)
if result.returncode != 0:
return {
"success": False,
"error": f"Tests failed before release:\n{result.stderr[-500:]}",
"steps": steps + [{"step": "tests", "status": "failed"}],
}
steps.append({"step": "tests", "status": "passed"})
# Step 3: Bump version
try:
from .version import bump_version # from Step 6 above
result = bump_version(project_dir, bump_type)
if not result["success"]:
return {
"success": False,
"error": f"Version bump failed: {result.get('error', 'unknown')}",
"steps": steps + [{"step": "version_bump", "status": "failed"}],
}
new_version = result.get("new_tag") or result.get("new_version", "unknown")
steps.append({"step": "version_bump", "status": "passed", "new_version": new_version})
except ImportError:
return {
"success": False,
"error": "bump_version function not found — define it first",
"steps": steps,
}
# Step 4: Build distribution
build_result = subprocess.run(
["uv", "build", "--sdist", "--wheel"],
cwd=project_dir,
capture_output=True,
text=True,
timeout=120,
)
if build_result.returncode != 0:
return {
"success": False,
"error": f"Build failed:\n{build_result.stderr[:500]}",
"steps": steps + [{"step": "build", "status": "failed"}],
}
steps.append({"step": "build", "status": "passed"})
# Step 5: Publish (unless skipped)
if not skip_publish:
publish_result = subprocess.run(
["uv", "publish"],
cwd=project_dir,
capture_output=True,
text=True,
timeout=120,
)
steps.append({
"step": "publish",
"status": "passed" if publish_result.returncode == 0 else "failed",
})
return {
"success": len(errors) == 0,
"new_version": new_version,
"steps": steps,
"errors": errors,
}
def _git_tag_and_push(version: str, project_dir: str) -> bool:
"""Create a git tag for the release version and push to remote."""
try:
subprocess.run(["git", "tag", f"v{version}"], cwd=project_dir, check=True)
subprocess.run(
["git", "push", "origin", f"v{version}"],
cwd=project_dir,
check=True,
)
return True
except subprocess.CalledProcessError as e:
print(f"Git tag/push failed: {e}")
return False
Constraints
MUST DO
- Use pyproject.toml as the single configuration file — no setup.py, no setup.cfg, no requirements.txt
- Choose hatchling as the default build backend unless C extensions or code generation require setuptools
- Pin
requires-pythonto a minimum version in every pyproject.toml (e.g.,>=3.12) - Commit the lockfile (
uv.lock) — never distribute without it, as builds become non-reproducible - Use
uv sync --lockedin CI to enforce lockfile determinism - Configure pytest with explicit testpaths and coverage thresholds (fail_under ≥ 80%)
- Version bump using atomic read/write to pyproject.toml or hatch-vcs git tags — never edit the version string manually without a tool
MUST NOT DO
- Use setuptools as the build backend unless C extensions, build-time code generation, or legacy compatibility require it
- Manually manage virtual environments with
python -m venv+source activate— uv handles this automatically - Omit the lockfile from version control — nondeterministic builds break CI and production deployments
- Pin every dependency to an exact version (
package==1.2.3) — use flexible constraints (>=1.2,<2.0) for compatibility - Publish a package with a pre-release version (alpha, beta, rc) unless intentionally deploying to PyPI test index
- Forget to clean the
dist/andbuild/directories before building — stale artifacts cause upload confusion
Output Template
When implementing or configuring Python packaging with this skill active, produce:
- Backend Recommendation — Selected build backend with justification based on project type (library, CLI tool, data science)
- Complete pyproject.toml — Full file content ready to write, with all sections: [build-system], [project], [tool.*] configurations
- Dependency Resolution Plan — uv commands needed for adding deps, syncing, and upgrading, plus lockfile strategy
- Test and Coverage Configuration — pytest settings, coverage thresholds, ruff/lint rules
- Publishing Checklist — Pre-publish verification steps, version bump method, git tag workflow
Related Skills
| Skill | Purpose |
|---|---|
coding-modern-python-development |
Modern Python 3.10+ coding patterns — the code written by this skill's projects |
coding-testing-patterns |
Pytest fixtures, parametrize, mocking strategies for testing packaged libraries |
coding-cve-dependency-management |
Scan published dependencies for known CVEs after packaging updates |
Live References
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.
- PEP 621 — Storing Project Metadata in pyproject.toml
- uv Documentation — Fast Python Package Manager
- Hatchling Build Backend Documentation
- Hatch-VCS — Dynamic Version Management from Git Tags
- PyPI Upload Guidelines — Publishing Your Package
- pytest Configuration Reference
- Ruff Linter — Fast Python linter and code formatter