npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-pythonWant just this skill?
Then install: npx claudepluginhub u/[userId]/[slug]
This skill should be used when creating or editing pyproject.toml, managing Python dependencies, configuring build systems, setting up uv workspaces, or publishing Python packages.
This skill uses the workspace's default tool permissions.
Packaging Conventions
This skill defines comprehensive conventions for Python project packaging, dependency management,
and distribution. These conventions prioritize modern packaging standards, reproducible builds, and
streamlined tooling using uv and pyproject.toml.
pyproject.toml is Canonical
RULE: All project configuration must live in pyproject.toml. No legacy configuration files:
# CORRECT: Everything in pyproject.toml
[project]
name = "mypackage"
version = "0.1.0"
description = "A sample Python package"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
keywords = ["example", "package"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"httpx>=0.24.0,<1.0",
"pydantic>=2.0,<3.0",
]
[project.optional-dependencies]
dev = [
"ruff>=0.1.0",
"mypy>=1.7.0",
"pre-commit>=3.5.0",
]
test = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.0",
"factory-boy>=3.3.0",
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.4.0",
]
[project.scripts]
myapp = "mypackage.cli:main"
[project.urls]
Homepage = "https://github.com/username/mypackage"
Documentation = "https://mypackage.readthedocs.io"
Repository = "https://github.com/username/mypackage"
Issues = "https://github.com/username/mypackage/issues"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-ra -q --strict-markers"
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
]
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
fail_under = 90
show_missing = true
Files to DELETE or never create:
# WRONG: These files should not exist
setup.py # Use pyproject.toml
setup.cfg # Use pyproject.toml
requirements.txt # Use pyproject.toml dependencies
dev-requirements.txt # Use [project.optional-dependencies]
mypy.ini # Use [tool.mypy] in pyproject.toml
.flake8 # Use [tool.ruff] in pyproject.toml
.isort.cfg # Use [tool.ruff.lint.isort] in pyproject.toml
pytest.ini # Use [tool.pytest.ini_options] in pyproject.toml
tox.ini # Use pyproject.toml or separate workflow
Source Layout
RULE: Always use src/ layout for packages:
# CORRECT: src layout
myproject/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── core.py
│ ├── models.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_core.py
├── docs/
│ └── index.md
├── pyproject.toml
├── uv.lock
├── .python-version
└── README.md
# WRONG: Flat layout
myproject/
├── mypackage/ # Package at root level
│ ├── __init__.py
│ └── core.py
├── tests/
├── pyproject.toml
└── README.md
Why src/ layout:
- Prevents accidentally importing from source instead of installed package
- Ensures tests run against installed package
- Clearer separation between package and project files
- Better for editable installs
- Industry standard practice
Package structure:
# src/mypackage/__init__.py
"""MyPackage - A sample Python package."""
from __future__ import annotations
from mypackage.core import main_function
from mypackage.models import User, Post
__version__ = "0.1.0"
__all__ = ["main_function", "User", "Post"]
Dependency Management
RULE: Use dependency groups in pyproject.toml with appropriate version constraints:
# CORRECT: Well-structured dependencies
[project]
name = "mypackage"
version = "0.1.0"
requires-python = ">=3.11"
# Core runtime dependencies
dependencies = [
"httpx>=0.24.0,<1.0", # Compatible version range
"pydantic>=2.0,<3.0",
"sqlalchemy>=2.0,<3.0",
"alembic>=1.12,<2.0",
]
[project.optional-dependencies]
# Development tools
dev = [
"ruff>=0.1.0",
"mypy>=1.7.0",
"pre-commit>=3.5.0",
"ipython>=8.17.0",
]
# Testing dependencies
test = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.0",
"pytest-mock>=3.12.0",
"factory-boy>=3.3.0",
"faker>=20.0.0",
]
# Documentation
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.4.0",
"mkdocstrings[python]>=0.24.0",
]
# Optional database backends
postgres = [
"psycopg[binary]>=3.1.0",
]
mysql = [
"mysqlclient>=2.2.0",
]
# All optional dependencies combined
all = [
"mypackage[postgres,mysql]",
]
Version pinning guidelines:
# CORRECT: Appropriate version constraints
dependencies = [
"httpx>=0.24.0,<1.0", # Major version constraint
"pydantic>=2.5.0,<3.0", # Minimum minor for required feature
"python-dateutil>=2.8.2", # Stable package, minimum version
]
# WRONG: Too restrictive or too loose
dependencies = [
"httpx==0.24.1", # Exact pin - prevents security updates
"pydantic>=2.0", # No upper bound - may break on v3
"requests", # No version constraint at all
]
When to use exact versions:
- In lock files (
uv.lock) only - For critical production deployments (with regular updates)
- Never in library packages (causes dependency conflicts)
Python Version Management
RULE: Specify Python version in both pyproject.toml and .python-version:
# pyproject.toml
[project]
requires-python = ">=3.11"
[tool.ruff]
target-version = "py311"
[tool.mypy]
python_version = "3.11"
# .python-version (for uv/pyenv)
3.11
Multiple Python version support:
[project]
requires-python = ">=3.11,<4.0"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
Testing multiple versions (in CI):
# .github/workflows/test.yml
strategy:
matrix:
python-version: ['3.11', '3.12']
Entry Points and Scripts
RULE: Define command-line interfaces using [project.scripts]:
# CORRECT: Console scripts
[project.scripts]
myapp = "mypackage.cli:main"
myapp-admin = "mypackage.admin:admin_main"
myapp-migrate = "mypackage.db.migrations:migrate"
[project.gui-scripts]
myapp-gui = "mypackage.gui:main" # For GUI applications
[project.entry-points."mypackage.plugins"]
# Plugin system entry points
json-plugin = "mypackage.plugins.json:JsonPlugin"
yaml-plugin = "mypackage.plugins.yaml:YamlPlugin"
CLI implementation:
# src/mypackage/cli.py
from __future__ import annotations
import sys
from pathlib import Path
import click
@click.group()
@click.version_option()
def main() -> None:
"""MyPackage command-line interface."""
@main.command()
@click.option("--config", type=click.Path(exists=True, path_type=Path))
def run(config: Path | None) -> None:
"""Run the application."""
click.echo(f"Running with config: {config}")
@main.command()
@click.argument("output", type=click.Path(path_type=Path))
def export(output: Path) -> None:
"""Export data to file."""
click.echo(f"Exporting to: {output}")
if __name__ == "__main__":
sys.exit(main())
After installation:
# Commands available in PATH
myapp --help
myapp run --config config.toml
myapp export output.json
Build Backend Configuration
RULE: Use modern build backends like hatchling or setuptools with pyproject.toml:
# CORRECT: Using hatchling (recommended for new projects)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
"/README.md",
"/LICENSE",
]
# ALTERNATIVE: Using setuptools (if needed for compatibility)
[build-system]
requires = ["setuptools>=68", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
Including data files:
[tool.hatch.build.targets.wheel.shared-data]
"data/templates" = "share/mypackage/templates"
"data/static" = "share/mypackage/static"
[tool.hatch.build.targets.wheel.force-include]
"config/default.toml" = "mypackage/default.toml"
uv Workspace Configuration
RULE: For monorepos, use uv workspaces to manage multiple packages:
# Monorepo structure
myproject/
├── pyproject.toml # Workspace root
├── uv.lock # Single lock file
├── packages/
│ ├── core/
│ │ ├── pyproject.toml
│ │ └── src/
│ │ └── myproject_core/
│ ├── api/
│ │ ├── pyproject.toml
│ │ └── src/
│ │ └── myproject_api/
│ └── cli/
│ ├── pyproject.toml
│ └── src/
│ └── myproject_cli/
└── tests/
# Root pyproject.toml
[tool.uv.workspace]
members = ["packages/*"]
[tool.uv.sources]
myproject-core = { workspace = true }
myproject-api = { workspace = true }
myproject-cli = { workspace = true }
# packages/api/pyproject.toml
[project]
name = "myproject-api"
version = "0.1.0"
dependencies = [
"myproject-core", # Workspace dependency
"fastapi>=0.104.0",
]
# packages/cli/pyproject.toml
[project]
name = "myproject-cli"
version = "0.1.0"
dependencies = [
"myproject-core", # Workspace dependency
"myproject-api",
"click>=8.1.0",
]
[project.scripts]
myproject = "myproject_cli.main:cli"
Working with workspaces:
# Install all workspace packages
uv sync
# Add dependency to specific package
uv add --package myproject-api httpx
# Run tests for specific package
uv run --package myproject-core pytest
# Build specific package
uv build --package myproject-api
Lock Files
RULE: Always commit uv.lock and regenerate after dependency changes:
# CORRECT: Lock file workflow
uv add httpx # Add dependency
uv lock # Update lock file
git add pyproject.toml uv.lock
git commit -m "Add httpx dependency"
# Update all dependencies to latest compatible versions
uv lock --upgrade
# Sync environment with lock file
uv sync
Lock file benefits:
- Reproducible installations across environments
- Faster installation (no resolution needed)
- Security auditing of exact versions
- Dependency tree documentation
Lock file in CI/CD:
# .github/workflows/test.yml
- name: Install dependencies
run: uv sync --frozen # Use exact versions from lock file
Common uv Commands
RULE: Use uv for all dependency and project management:
# Project initialization
uv init myproject # Create new project
uv init --lib mypackage # Create new library
uv init --app myapp # Create new application
# Dependency management
uv add httpx # Add dependency
uv add --dev pytest # Add dev dependency
uv add --optional postgres psycopg # Add optional dependency
uv add "httpx>=0.24.0,<1.0" # Add with version constraint
uv remove httpx # Remove dependency
uv tree # Show dependency tree
# Environment management
uv sync # Install all dependencies
uv sync --all-extras # Install with all optional deps
uv sync --frozen # Install from lock without updating
uv sync --no-dev # Install without dev dependencies
# Running commands
uv run python script.py # Run script in project environment
uv run pytest # Run tests
uv run mypy src/ # Run type checking
uv run python -m mypackage.cli # Run module
# Lock file operations
uv lock # Generate/update lock file
uv lock --upgrade # Upgrade all dependencies
uv lock --upgrade-package httpx # Upgrade specific package
# Build and publish
uv build # Build wheel and sdist
uv publish # Publish to PyPI
uv publish --token $TOKEN # Publish with token
# Python version management
uv python install 3.12 # Install Python 3.12
uv python list # List available Python versions
uv venv --python 3.12 # Create venv with specific version
Complete pyproject.toml Examples
Example 1: FastAPI Service
[project]
name = "myapi"
version = "0.1.0"
description = "RESTful API service"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
dependencies = [
"fastapi>=0.104.0,<1.0",
"uvicorn[standard]>=0.24.0,<1.0",
"pydantic>=2.5.0,<3.0",
"pydantic-settings>=2.1.0,<3.0",
"sqlalchemy>=2.0,<3.0",
"alembic>=1.13.0,<2.0",
"psycopg[binary]>=3.1.0,<4.0",
"python-jose[cryptography]>=3.3.0,<4.0",
"passlib[bcrypt]>=1.7.4,<2.0",
"httpx>=0.25.0,<1.0",
]
[project.optional-dependencies]
dev = [
"ruff>=0.1.0",
"mypy>=1.7.0",
"pre-commit>=3.5.0",
]
test = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.0",
"factory-boy>=3.3.0",
]
[project.scripts]
myapi = "myapi.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/myapi"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "C4", "SIM"]
ignore = ["E501"]
[tool.mypy]
python_version = "3.11"
plugins = ["pydantic.mypy"]
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-ra -q --strict-markers --cov=src --cov-report=html --cov-report=term"
[tool.coverage.run]
source = ["src"]
branch = true
omit = ["*/tests/*", "*/migrations/*"]
[tool.coverage.report]
fail_under = 90
show_missing = true
Example 2: CLI Application
[project]
name = "mycli"
version = "0.1.0"
description = "Command-line tool for data processing"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
dependencies = [
"click>=8.1.0,<9.0",
"rich>=13.7.0,<14.0",
"pydantic>=2.5.0,<3.0",
"httpx>=0.25.0,<1.0",
"python-dateutil>=2.8.2",
]
[project.optional-dependencies]
dev = [
"ruff>=0.1.0",
"mypy>=1.7.0",
]
test = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
]
[project.scripts]
mycli = "mycli.main:cli"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mycli"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.mypy]
python_version = "3.11"
warn_return_any = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
Example 3: Python Library
[project]
name = "mylib"
version = "0.1.0"
description = "Reusable Python library"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
keywords = ["library", "utilities"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
# Minimal dependencies for libraries
dependencies = [
"typing-extensions>=4.8.0; python_version < '3.12'",
]
[project.optional-dependencies]
dev = [
"ruff>=0.1.0",
"mypy>=1.7.0",
"pre-commit>=3.5.0",
]
test = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"hypothesis>=6.92.0",
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.4.0",
"mkdocstrings[python]>=0.24.0",
]
[project.urls]
Homepage = "https://github.com/username/mylib"
Documentation = "https://mylib.readthedocs.io"
Repository = "https://github.com/username/mylib"
Issues = "https://github.com/username/mylib/issues"
Changelog = "https://github.com/username/mylib/blob/main/CHANGELOG.md"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/mylib/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/mylib"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q --strict-markers --cov=src --cov-report=html"
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
fail_under = 95
show_missing = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"@abstractmethod",
]
Publishing Packages
RULE: Follow standard publishing workflow:
# 1. Update version in pyproject.toml
# 2. Update CHANGELOG.md
# 3. Commit and tag
git add pyproject.toml CHANGELOG.md
git commit -m "Release v0.1.0"
git tag v0.1.0
# 4. Build package
uv build
# Verify build artifacts
ls dist/
# mypackage-0.1.0-py3-none-any.whl
# mypackage-0.1.0.tar.gz
# 5. Publish to TestPyPI first
uv publish --publish-url https://test.pypi.org/legacy/ \
--token $TEST_PYPI_TOKEN
# 6. Test installation from TestPyPI
uv pip install --index-url https://test.pypi.org/simple/ mypackage
# 7. Publish to PyPI
uv publish --token $PYPI_TOKEN
# 8. Push tags
git push origin v0.1.0
GitHub Actions for publishing:
# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Build package
run: uv build
- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish
Metadata Best Practices
RULE: Provide comprehensive package metadata:
[project]
name = "mypackage"
version = "0.1.0"
description = "Clear one-line description of what package does"
authors = [
{name = "Primary Author", email = "author@example.com"},
{name = "Contributor Name"},
]
maintainers = [
{name = "Maintainer Name", email = "maintainer@example.com"},
]
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
keywords = ["specific", "searchable", "keywords"]
classifiers = [
# Development status
"Development Status :: 4 - Beta",
# Audience
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
# License
"License :: OSI Approved :: MIT License",
# Python versions
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
# Topics
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet :: WWW/HTTP",
]
[project.urls]
Homepage = "https://mypackage.example.com"
Documentation = "https://docs.mypackage.example.com"
Repository = "https://github.com/username/mypackage"
Issues = "https://github.com/username/mypackage/issues"
Changelog = "https://github.com/username/mypackage/blob/main/CHANGELOG.md"
Anti-Patterns to Avoid
1. Mixing Configuration Locations
# WRONG: Configuration scattered across multiple files
setup.py
setup.cfg
requirements.txt
dev-requirements.txt
mypy.ini
.flake8
# CORRECT: Everything in pyproject.toml
pyproject.toml
2. Incorrect Dependency Pinning
# WRONG: Too restrictive for libraries
dependencies = [
"requests==2.31.0", # Exact pin causes conflicts
]
# WRONG: No version constraints
dependencies = [
"requests", # Any version - may break
]
# CORRECT: Compatible range
dependencies = [
"requests>=2.31.0,<3.0",
]
3. Flat Package Layout
# WRONG: Package at project root
myproject/
├── mypackage/ # Confusing - easy to import from wrong location
│ └── __init__.py
├── tests/
└── pyproject.toml
# CORRECT: src/ layout
myproject/
├── src/
│ └── mypackage/ # Clear separation
│ └── __init__.py
├── tests/
└── pyproject.toml
4. Not Committing Lock Files
# WRONG: Ignoring lock files
echo "uv.lock" >> .gitignore
# CORRECT: Commit lock files for reproducibility
git add uv.lock
git commit -m "Update dependencies"
5. Using setup.py for Configuration
# WRONG: Using setup.py
from setuptools import setup
setup(
name="mypackage",
version="0.1.0",
# ... configuration
)
# CORRECT: Use pyproject.toml
[project]
name = "mypackage"
version = "0.1.0"
Summary Checklist
When setting up Python packaging, ensure:
- All configuration in
pyproject.toml - Using
src/layout for packages - No legacy files (setup.py, requirements.txt, etc.)
-
requires-pythonspecifies minimum Python version -
.python-versionfile for uv/pyenv - Dependencies use compatible version ranges
- Dependency groups for dev, test, docs
- Entry points defined in
[project.scripts] - Build backend configured (hatchling or setuptools)
-
uv.lockcommitted to repository - Using
uvfor all dependency operations - Comprehensive package metadata
- Tool configurations in respective
[tool.*]sections - For monorepos, workspace properly configured
- README.md and LICENSE files present
These conventions ensure Python packages are well-structured, maintainable, and follow modern packaging standards.
Similar Skills
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.