Skill
Community

packaging-conventions

Install
1
Install the plugin
$
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-python

Want just this skill?

Then install: npx claudepluginhub u/[userId]/[slug]

Description

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.

Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

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-python specifies minimum Python version
  • .python-version file 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.lock committed to repository
  • Using uv for 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.

Stats
Stars0
Forks0
Last CommitFeb 9, 2026

Similar Skills