Claude Code Plugins

Community-maintained marketplace

Feedback

project-structure-and-tooling

@tachyon-beep/skillpacks
2
0

Modern Python project structure, pyproject.toml, ruff, mypy, pre-commit hooks, dependency management, packaging

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name project-structure-and-tooling
description Modern Python project structure, pyproject.toml, ruff, mypy, pre-commit hooks, dependency management, packaging

Project Structure and Tooling

Overview

Core Principle: Project setup is infrastructure. Good infrastructure is invisible when working, painful when missing. Set it up once, benefit forever.

Modern Python projects use pyproject.toml for all configuration, ruff for linting and formatting, mypy for type checking, and pre-commit for automated quality gates. The choice between src layout and flat layout determines import patterns and package discoverability.

This skill covers SETUP of tooling. For FIXING lint warnings systematically, see systematic-delinting.

When to Use

Use this skill when:

  • Starting a new Python project
  • "How should I structure my project?"
  • Setting up pyproject.toml
  • Configuring ruff, mypy, or pre-commit
  • "What dependency manager should I use?"
  • Packaging Python projects for distribution

Don't use when:

  • Fixing existing lint warnings (use systematic-delinting)
  • Writing type hints (use modern-syntax-and-types)
  • Setting up tests (use testing-and-quality)

Symptoms triggering this skill:

  • "New Python project setup"
  • "Configure ruff/black/mypy"
  • "src layout vs flat layout"
  • "Poetry vs pip-tools"
  • "Package my project"

Project Layout Decisions

Src Layout vs Flat Layout

Decision tree:

Distributing as package? → src layout
Testing import behavior? → src layout
Simple script/app? → flat layout
Learning project? → flat layout
Production library? → src layout

Flat Layout

my_project/
├── pyproject.toml
├── README.md
├── my_package/
│   ├── __init__.py
│   ├── module1.py
│   └── module2.py
└── tests/
    ├── __init__.py
    ├── test_module1.py
    └── test_module2.py

Pros:

  • Simpler structure
  • Easier to understand for beginners
  • Fewer directories

Cons:

  • Can accidentally import from source instead of installed package
  • Harder to test actual install behavior
  • Package and project root mixed

Use when:

  • Simple applications
  • Learning projects
  • Not distributing as package

Src Layout (Recommended for Libraries)

my_project/
├── pyproject.toml
├── README.md
├── src/
│   └── my_package/
│       ├── __init__.py
│       ├── module1.py
│       └── module2.py
└── tests/
    ├── __init__.py
    ├── test_module1.py
    └── test_module2.py

Pros:

  • Forces testing against installed package
  • Clear separation: src/ is package, tests/ is tests
  • Prevents accidental imports from source
  • Industry standard for libraries

Cons:

  • One extra directory level
  • Slightly more complex

Use when:

  • Creating a library
  • Distributing on PyPI
  • Want production-quality setup

Why this matters: Src layout forces you to install your package in editable mode (pip install -e .), ensuring tests run against the installed package, not loose Python files. Catches import issues early.


pyproject.toml Fundamentals

Basic Structure

File: pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-package"
version = "0.1.0"
description = "A short description"
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
readme = "README.md"
requires-python = ">=3.12"
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "Programming Language :: Python :: 3.12",
]
dependencies = [
    "requests>=2.31.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.4.0",
    "mypy>=1.5.0",
    "ruff>=0.1.0",
]

[project.urls]
Homepage = "https://github.com/username/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/username/my-package"

[tool.ruff]
target-version = "py312"
line-length = 140

[tool.mypy]
python_version = "3.12"
strict = true

Why this matters: Single file for all configuration. No setup.py, setup.cfg, or scattered config files. Modern standard (PEP 621).

Build System Selection

hatchling (recommended for most projects):

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

setuptools (traditional, still common):

[build-system]
requires = ["setuptools>=68.0.0", "wheel"]
build-backend = "setuptools.build_meta"

poetry (if using Poetry for dependencies):

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Decision tree:

Using Poetry for deps? → poetry-core
Need advanced features? → setuptools
Simple project? → hatchling

Why hatchling?

  • Modern, fast, minimal configuration
  • Good defaults
  • Works with standard tools
  • No legacy baggage

Version Management

Static version:

[project]
version = "0.1.0"

Dynamic version from file:

[project]
dynamic = ["version"]

[tool.hatch.version]
path = "src/my_package/__init__.py"

File: src/my_package/__init__.py

__version__ = "0.1.0"

Dynamic version from git tag:

[build-system]
requires = ["hatchling", "hatch-vcs"]

[tool.hatch.version]
source = "vcs"

Recommendation: Start with static version. Add dynamic versioning when you need it.


Ruff Configuration

Core Configuration

File: pyproject.toml

[tool.ruff]
target-version = "py312"
line-length = 140  # Note: 140, not default 88

# Exclude patterns
exclude = [
    ".git",
    ".venv",
    "__pycache__",
    "build",
    "dist",
    "*.egg-info",
]

[tool.ruff.lint]
# Enable rule sets
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "N",   # pep8-naming
    "UP",  # pyupgrade
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "SIM", # flake8-simplify
    "RUF", # ruff-specific
]

# Ignore specific rules
ignore = [
    "E501",  # Line too long (handled by formatter)
]

# Per-file ignores
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
    "S101",  # Allow assert in tests
]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"

Why line-length = 140?

  • Modern screens are wide
  • Default 88 is too restrictive for complex type hints
  • 140 balances readability and fitting multiple windows
  • Industry trend toward 100-140

Rule set breakdown:

Set Purpose Example Rules
E/W PEP 8 style Whitespace, indentation
F Logical errors Undefined names, unused imports
I Import sorting isort compatibility
N Naming PEP 8 naming conventions
UP Python upgrades Use Python 3.10+ features
B Bug detection Likely bugs (mutable defaults)
C4 Comprehensions Better list/dict comprehensions
SIM Simplification Simplify complex code
RUF Ruff-specific Ruff's custom checks

Import Sorting (isort compatibility)

[tool.ruff.lint.isort]
known-first-party = ["my_package"]
known-third-party = ["numpy", "pandas"]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
lines-after-imports = 2

Expected import order:

# Future imports
from __future__ import annotations

# Standard library
import json
import sys
from pathlib import Path

# Third-party
import numpy as np
import pandas as pd
import requests

# First-party
from my_package import utils
from my_package.core import Engine


def my_function():
    ...

Why this matters: Consistent import ordering improves readability and prevents merge conflicts.

Advanced Configuration

[tool.ruff.lint.flake8-bugbear]
# Extend immutable calls (prevent mutation)
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]

[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"

[tool.ruff.lint.mccabe]
max-complexity = 10

[tool.ruff.lint.pydocstyle]
convention = "google"  # or "numpy", "pep257"

Complexity limit explanation:

  • Complexity < 10: Good
  • 10-15: Acceptable, monitor
  • 15+: Refactor

Type Checking with mypy

Strict Configuration

File: pyproject.toml

[tool.mypy]
python_version = "3.12"
strict = true

# Strict mode includes:
# - warn_return_any
# - warn_unused_configs
# - disallow_untyped_defs
# - disallow_any_generics
# - disallow_subclassing_any
# - disallow_untyped_calls
# - disallow_untyped_decorators
# - disallow_incomplete_defs
# - check_untyped_defs
# - warn_redundant_casts
# - warn_unused_ignores
# - warn_no_return
# - warn_unreachable
# - strict_equality

# Exclude patterns
exclude = [
    "^build/",
    "^dist/",
]

# Per-module overrides
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false  # Tests can be less strict

[[tool.mypy.overrides]]
module = "third_party.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "untyped_library"
ignore_missing_imports = true

Incremental Adoption

Start lenient, get stricter:

# Phase 1: Basic type checking
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true

# Phase 2: Add more checks
check_untyped_defs = true
warn_redundant_casts = true
warn_unused_ignores = true

# Phase 3: Require types
disallow_untyped_defs = true
disallow_incomplete_defs = true

# Phase 4: Full strict
strict = true

Per-module migration:

[tool.mypy]
python_version = "3.12"
# Default: lenient

[[tool.mypy.overrides]]
module = "my_package.new_module"
strict = true  # New code is strict

[[tool.mypy.overrides]]
module = "my_package.legacy"
ignore_errors = true  # TODO: Fix legacy code

Why this matters: Incremental adoption prevents overwhelming backlog of type errors. Strict mode for new code, lenient for legacy.


Dependency Management

pip-tools

Recommended for most projects. Simple, standard, no lock-in.

Setup:

pip install pip-tools

File: requirements.in (high-level dependencies)

requests>=2.31.0
pydantic>=2.0.0

Generate locked requirements:

pip-compile requirements.in
# Creates requirements.txt with exact versions

File: requirements.txt (auto-generated)

certifi==2023.7.22
    # via requests
charset-normalizer==3.2.0
    # via requests
idna==3.4
    # via requests
pydantic==2.3.0
    # via -r requirements.in
pydantic-core==2.6.3
    # via pydantic
requests==2.31.0
    # via -r requirements.in
urllib3==2.0.4
    # via requests

Development dependencies:

File: requirements-dev.in

-c requirements.txt  # Constrain to production versions
pytest>=7.4.0
mypy>=1.5.0
ruff>=0.1.0

Compile:

pip-compile requirements-dev.in

Sync environment:

pip-sync requirements.txt requirements-dev.txt

Why pip-tools?

  • Uses standard requirements.txt format
  • No proprietary lock file
  • Simple mental model
  • Works everywhere
  • No lock-in

Poetry

Better for libraries, more features, heavier.

Setup:

curl -sSL https://install.python-poetry.org | python3 -

File: pyproject.toml

[tool.poetry]
name = "my-package"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.12"
requests = "^2.31.0"
pydantic = "^2.0.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
mypy = "^1.5.0"
ruff = "^0.1.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Commands:

poetry install          # Install dependencies
poetry add requests     # Add dependency
poetry add --group dev pytest  # Add dev dependency
poetry update           # Update dependencies
poetry lock             # Update lock file
poetry build            # Build package
poetry publish          # Publish to PyPI

Why Poetry?

  • Manages dependencies AND build system
  • Better dependency resolution
  • Built-in virtual environment management
  • Integrated publishing

Why NOT Poetry?

  • Heavier tool
  • Proprietary lock format
  • Slower than pip-tools
  • Lock-in to Poetry workflow

Comparison Decision Tree

Publishing to PyPI? → Poetry (integrated workflow)
Simple project? → pip-tools (minimal)
Need reproducible builds? → Either (both lock)
Team unfamiliar with tools? → pip-tools (simpler)
Complex dependency constraints? → Poetry (better resolver)
CI/CD integration? → pip-tools (faster)

Pre-commit Hooks

Setup

Install:

pip install pre-commit

File: .pre-commit-config.yaml

repos:
  # Ruff for linting and formatting
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.1.6
    hooks:
      # Run linter
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      # Run formatter
      - id: ruff-format

  # mypy for type checking
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.7.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]

  # Standard pre-commit hooks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
        args: [--maxkb=1000]
      - id: check-merge-conflict
      - id: check-case-conflict

  # Python-specific
  - repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
      - id: isort
        name: isort (python)

Install hooks:

pre-commit install

Run manually:

pre-commit run --all-files

Update hooks:

pre-commit autoupdate

Hook Selection Strategy

Essential hooks (always use):

  • ruff - Linting and formatting
  • trailing-whitespace - Clean files
  • end-of-file-fixer - Proper file endings
  • check-yaml - YAML syntax
  • check-merge-conflict - Prevent merge markers

Recommended hooks:

  • mypy - Type checking
  • check-toml - pyproject.toml syntax
  • check-added-large-files - Prevent large files

Optional hooks:

  • pytest - Run tests (slow!)
  • bandit - Security checks
  • interrogate - Docstring coverage

Why NOT include slow hooks:

# ❌ WRONG: Tests in pre-commit (too slow)
- repo: local
  hooks:
    - id: pytest
      name: pytest
      entry: pytest
      language: system
      pass_filenames: false

Why this matters: Pre-commit hooks run on EVERY commit. Keep them fast (<5 seconds total). Run tests in CI, not pre-commit.

Skipping Hooks

Skip all hooks (use sparingly):

git commit --no-verify -m "Quick fix"

Skip specific hook:

SKIP=mypy git commit -m "WIP: type errors to fix"

When to skip:

  • WIP commits on feature branch (will fix before PR)
  • Emergency hotfixes (fix hooks after)
  • Known false positives (fix hook config instead)

When NOT to skip:

  • Merging to main
  • Creating PR
  • "Too lazy to fix" ← Never valid reason

Formatting and Linting Workflow

Ruff as Formatter and Linter

Ruff replaces: black, isort, flake8, pyupgrade, and more.

Format code:

ruff format .

Check linting:

ruff check .

Fix auto-fixable issues:

ruff check --fix .

Show what would fix without changing:

ruff check --fix --diff .

IDE Integration

VS Code (.vscode/settings.json):

{
  "python.linting.enabled": true,
  "python.linting.ruffEnabled": true,
  "python.formatting.provider": "none",
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "charliermarsh.ruff",
  "[python]": {
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.fixAll": true,
      "source.organizeImports": true
    },
    "editor.defaultFormatter": "charliermarsh.ruff"
  }
}

PyCharm:

  • Install Ruff plugin
  • Settings → Tools → Ruff → Enable
  • Settings → Tools → Actions on Save → Ruff format

Why this matters: Format on save prevents formatting commits. Linting in IDE catches issues before commit.


Packaging and Distribution

Minimal Package

File structure:

my_package/
├── pyproject.toml
├── README.md
├── LICENSE
└── src/
    └── my_package/
        ├── __init__.py
        └── main.py

File: pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-package"
version = "0.1.0"
description = "A short description"
readme = "README.md"
requires-python = ">=3.12"
license = {text = "MIT"}
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
classifiers = [
    "Programming Language :: Python :: 3.12",
]
dependencies = []

[project.urls]
Homepage = "https://github.com/username/my-package"

Build:

pip install build
python -m build

Creates:

dist/
├── my_package-0.1.0-py3-none-any.whl
└── my_package-0.1.0.tar.gz

Publishing to PyPI

Test on TestPyPI first:

pip install twine

# Upload to TestPyPI
twine upload --repository testpypi dist/*

# Test install
pip install --index-url https://test.pypi.org/simple/ my-package

Publish to real PyPI:

twine upload dist/*

Better: Use GitHub Actions

File: .github/workflows/publish.yml

name: Publish to PyPI

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: "3.12"

      - name: Install build
        run: pip install build twine

      - name: Build package
        run: python -m build

      - name: Publish to PyPI
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
        run: twine upload dist/*

Why this matters: Automated publishing on GitHub release. Consistent process, no manual uploads.

Entry Points

Console scripts:

[project.scripts]
my-cli = "my_package.cli:main"
my-tool = "my_package.tools:run"

Creates command-line tools:

pip install my-package
my-cli --help  # Runs my_package.cli:main()

File: src/my_package/cli.py

def main() -> None:
    print("Hello from my-cli!")

if __name__ == "__main__":
    main()

Complete Example: Production Project

Project Structure

awesome_project/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── publish.yml
├── .pre-commit-config.yaml
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── src/
│   └── awesome_project/
│       ├── __init__.py
│       ├── core.py
│       ├── utils.py
│       └── py.typed
└── tests/
    ├── __init__.py
    ├── test_core.py
    └── test_utils.py

pyproject.toml (Complete)

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "awesome-project"
version = "0.1.0"
description = "An awesome Python project"
readme = "README.md"
requires-python = ">=3.12"
license = {text = "MIT"}
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3 :: Only",
    "Typing :: Typed",
]
dependencies = [
    "requests>=2.31.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.4.0",
    "pytest-cov>=4.1.0",
    "mypy>=1.5.0",
    "ruff>=0.1.0",
    "pre-commit>=3.5.0",
    "types-requests>=2.31.0",
]

[project.urls]
Homepage = "https://github.com/username/awesome-project"
Documentation = "https://awesome-project.readthedocs.io"
Repository = "https://github.com/username/awesome-project"
Issues = "https://github.com/username/awesome-project/issues"

[project.scripts]
awesome = "awesome_project.cli:main"

# Ruff configuration
[tool.ruff]
target-version = "py312"
line-length = 140

exclude = [
    ".git",
    ".venv",
    "__pycache__",
    "build",
    "dist",
    "*.egg-info",
]

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "N",   # pep8-naming
    "UP",  # pyupgrade
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "SIM", # flake8-simplify
    "RUF", # ruff-specific
]

ignore = [
    "E501",  # Line too long (handled by formatter)
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
    "S101",  # Allow assert in tests
]

[tool.ruff.lint.isort]
known-first-party = ["awesome_project"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

# mypy configuration
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

# pytest configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = [
    "--strict-markers",
    "--strict-config",
    "--cov=awesome_project",
    "--cov-report=term-missing",
]

# Coverage configuration
[tool.coverage.run]
source = ["src"]
omit = ["tests/*"]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
]

.pre-commit-config.yaml (Complete)

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.1.6
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.7.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests, pydantic]

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
      - id: check-merge-conflict

.gitignore (Complete)

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
venv/
env/
ENV/
.venv

# IDEs
.vscode/
.idea/
*.swp
*.swo
*~

# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Ruff
.ruff_cache/

# OS
.DS_Store
Thumbs.db

CI Workflow

File: .github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.12"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          pip install -e ".[dev]"

      - name: Run ruff (lint)
        run: ruff check .

      - name: Run ruff (format check)
        run: ruff format --check .

      - name: Run mypy
        run: mypy src/

      - name: Run pytest
        run: pytest --cov --cov-report=xml

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        if: matrix.os == 'ubuntu-latest'

Anti-Patterns

Scattered Configuration Files

# ❌ WRONG: Configuration in multiple files
setup.py
setup.cfg
requirements.txt
requirements-dev.txt
.flake8
mypy.ini
pytest.ini
.isort.cfg
# ✅ CORRECT: Single pyproject.toml
# All configuration in one place
[tool.ruff]
...

[tool.mypy]
...

[tool.pytest.ini_options]
...

Why this matters: Single source of truth. Easier to maintain, version control, and share.

Not Using Src Layout for Libraries

# ❌ WRONG: Flat layout for distributed package
my_package/
├── my_package/
│   └── __init__.py
└── tests/

Problem: Tests might pass locally but fail when installed:

# Works locally (imports from source)
pytest  # PASS

# Fails when installed (package not installed correctly)
pip install .
python -c "import my_package"  # ImportError
# ✅ CORRECT: Src layout forces proper install
my_package/
├── src/
│   └── my_package/
│       └── __init__.py
└── tests/

Why this matters: Src layout catches packaging issues early by forcing editable install.

Too Many Dependencies

# ❌ WRONG: Kitchen sink approach
dependencies = [
    "requests",
    "httpx",       # Both requests and httpx?
    "urllib3",     # Already included with requests
    "pandas",
    "polars",      # Both pandas and polars?
    "numpy",       # Included with pandas
    # ... 50 more
]
# ✅ CORRECT: Minimal direct dependencies
dependencies = [
    "requests>=2.31.0",  # Only what YOU directly use
    "pydantic>=2.0.0",
]

# Transitive deps (requests → urllib3) handled automatically

Why this matters: More dependencies = more conflict risk, slower installs, larger attack surface.

Ignoring Lock Files

# ❌ WRONG: Install from requirements.in
pip install -r requirements.in

Problem: Gets different versions each time, breaks reproducibility.

# ✅ CORRECT: Install from locked requirements
pip install -r requirements.txt

Why this matters: Locked dependencies ensure reproducible builds and deployments.

Pre-commit Hooks Too Slow

# ❌ WRONG: Run full test suite on every commit
repos:
  - repo: local
    hooks:
      - id: pytest
        name: pytest
        entry: pytest tests/
        language: system
        pass_filenames: false

Problem: 5-minute test suite blocks every commit. Developers will skip hooks.

# ✅ CORRECT: Fast checks only
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.1.6
    hooks:
      - id: ruff
      - id: ruff-format

Why this matters: Pre-commit must be fast (<5s total). Run tests in CI, not pre-commit.


Decision Trees

Choosing Project Layout

├─ Distributing as package?
│  ├─ Yes → src layout
│  └─ No
│     ├─ Complex project? → src layout (future-proof)
│     └─ Simple script? → flat layout

Choosing Dependency Manager

├─ Publishing to PyPI?
│  ├─ Yes → Poetry (integrated workflow)
│  └─ No
│     ├─ Need simple workflow? → pip-tools
│     ├─ Complex constraints? → Poetry
│     └─ Existing requirements.txt? → pip-tools

Choosing Build Backend

├─ Using Poetry? → poetry-core
├─ Need setuptools features? → setuptools
└─ Simple project? → hatchling

Line Length Configuration

├─ Team preference for 88? → 88
├─ Complex type hints? → 120-140
├─ Modern screens? → 120-140
└─ No strong opinion? → 120

Common Workflows

New Project from Scratch

# 1. Create structure
mkdir my_project
cd my_project
git init

# 2. Create directory structure
mkdir -p src/my_project tests

# 3. Create pyproject.toml (see example above)
# 4. Create .pre-commit-config.yaml (see example above)
# 5. Create .gitignore (see example above)

# 6. Initialize package
cat > src/my_project/__init__.py << 'EOF'
"""My awesome project."""
__version__ = "0.1.0"
EOF

# 7. Create py.typed marker for type checking
touch src/my_project/py.typed

# 8. Install in editable mode
pip install -e ".[dev]"

# 9. Install pre-commit hooks
pre-commit install

# 10. First commit
git add .
git commit -m "feat: Initial project structure"

Adding Ruff to Existing Project

# 1. Install ruff
pip install ruff

# 2. Add to pyproject.toml
cat >> pyproject.toml << 'EOF'
[tool.ruff]
target-version = "py312"
line-length = 140

[tool.ruff.lint]
select = ["E", "W", "F", "I", "N", "UP", "B", "C4", "SIM", "RUF"]
ignore = ["E501"]
EOF

# 3. Check what would change
ruff check --diff .

# 4. Apply fixes
ruff check --fix .

# 5. Format code
ruff format .

# 6. Add to pre-commit
cat >> .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.1.6
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format
EOF

pre-commit install

Migrating from Black/Flake8 to Ruff

# 1. Install ruff
pip install ruff

# 2. Remove old tools
pip uninstall black flake8 isort pyupgrade

# 3. Convert black config to ruff
# Old .flake8:
# [flake8]
# max-line-length = 88
# ignore = E203, W503

# New pyproject.toml:
[tool.ruff]
line-length = 88

[tool.ruff.lint]
ignore = ["E203", "W503"]

# 4. Remove old config files
rm .flake8 .isort.cfg

# 5. Update pre-commit
# Replace black, isort, flake8 hooks with ruff

# 6. Reformat everything
ruff format .

Integration with Other Skills

Before using this skill:

  • No prerequisites (start here for new projects)

After using this skill:

  • Fix lint warnings → See systematic-delinting
  • Add type hints → See modern-syntax-and-types
  • Setup testing → See testing-and-quality
  • Add CI/CD → (Future skill)

Cross-references:

  • Type checking setup → modern-syntax-and-types for type hint patterns
  • Delinting process → systematic-delinting for fixing warnings
  • Testing setup → testing-and-quality for pytest configuration

Quick Reference

Essential Commands

# Project setup
pip install -e ".[dev]"       # Editable install with dev deps
pre-commit install            # Install git hooks

# Daily workflow
ruff check .                  # Lint
ruff check --fix .           # Lint and auto-fix
ruff format .                # Format
mypy src/                    # Type check
pytest                       # Run tests

# Pre-commit
pre-commit run --all-files   # Run all hooks manually
pre-commit autoupdate        # Update hook versions

# Dependency management (pip-tools)
pip-compile requirements.in              # Lock dependencies
pip-compile requirements-dev.in          # Lock dev dependencies
pip-sync requirements.txt requirements-dev.txt  # Sync environment

# Building and publishing
python -m build              # Build package
twine upload dist/*          # Upload to PyPI

Configuration Checklist

Minimum viable pyproject.toml:

  • [build-system] - hatchling or setuptools
  • [project] - name, version, dependencies
  • [tool.ruff] - target-version, line-length
  • [tool.mypy] - python_version, strict

Production-ready additions:

  • [project.optional-dependencies] - dev dependencies
  • [project.scripts] - console scripts
  • [tool.ruff.lint] - rule selection
  • [tool.pytest.ini_options] - test configuration
  • .pre-commit-config.yaml - automated checks
  • .gitignore - ignore build artifacts
  • src/package/py.typed - typed package marker

Ruff Rule Sets Quick Reference

Code Name Purpose
E/W pycodestyle PEP 8 style
F Pyflakes Logical errors
I isort Import ordering
N pep8-naming Naming conventions
UP pyupgrade Modern syntax
B flake8-bugbear Bug detection
C4 flake8-comprehensions Better comprehensions
SIM flake8-simplify Code simplification
RUF Ruff Ruff-specific

Enable progressively:

  1. Start: ["E", "W", "F"] - Core errors
  2. Add: ["I", "N", "UP"] - Style and modernization
  3. Add: ["B", "C4", "SIM"] - Quality improvements
  4. Add: ["RUF"] - Ruff-specific checks

Why This Matters: Real-World Impact

Good tooling setup prevents:

  • ❌ "Works on my machine" - Locked dependencies ensure consistency
  • ❌ Import errors in production - Src layout catches packaging issues
  • ❌ Style arguments in PRs - Automated formatting ends debates
  • ❌ Type errors in production - mypy catches before deploy
  • ❌ Breaking dependencies - Lock files ensure reproducibility
  • ❌ Manual quality checks - Pre-commit automates enforcement

Good tooling setup enables:

  • ✅ Fast onboarding - pip install -e ".[dev]" gets developers running
  • ✅ Consistent code style - Ruff format ensures uniformity
  • ✅ Early bug detection - Type checking and linting catch issues
  • ✅ Confident refactoring - Types and tests enable safe changes
  • ✅ Automated publishing - CI/CD handles releases
  • ✅ Professional polish - Well-configured projects attract contributors

Time investment:

  • Initial setup: 1-2 hours
  • Saved per month: 10+ hours (no style debates, fewer bugs, faster onboarding)
  • ROI: Positive after first month, compounds over project lifetime