From ccfg-python
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.
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-pythonThis skill uses the workspace's default tool permissions.
This skill defines comprehensive conventions for Python project packaging, dependency management,
Build and publish Python packages with uv: pyproject.toml configuration, versioning, entry points, wheels/sdists, and PyPI deployment.
Sets up modern Python projects with uv package manager for fast dependencies, pyproject.toml config, virtual environments, ruff linting/formatting, src layout, and PyPI publishing.
Guides Python package creation and distribution using pyproject.toml, uv for init/build/publish, entry points, PyPI upload, and CI/CD setup.
Share bugs, ideas, or general feedback.
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.
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
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:
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"]
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:
uv.lock) onlyRULE: 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']
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
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"
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
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:
Lock file in CI/CD:
# .github/workflows/test.yml
- name: Install dependencies
run: uv sync --frozen # Use exact versions from lock file
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
[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
[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"]
[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",
]
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
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"
# 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
# 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",
]
# 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
# WRONG: Ignoring lock files
echo "uv.lock" >> .gitignore
# CORRECT: Commit lock files for reproducibility
git add uv.lock
git commit -m "Update dependencies"
# 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"
When setting up Python packaging, ensure:
pyproject.tomlsrc/ layout for packagesrequires-python specifies minimum Python version.python-version file for uv/pyenv[project.scripts]uv.lock committed to repositoryuv for all dependency operations[tool.*] sectionsThese conventions ensure Python packages are well-structured, maintainable, and follow modern packaging standards.