← Back to Blog

A pyproject.toml boilerplate

Pythonpyproject.toml

Pyproject.toml boilerplate

Here is an example of boilerplate I might use for a new project.

# Boilerplate pyproject.toml — replace "my-project" and "my_project" with your names.
# See OPERATIONALIZATION.md for how to use this with uv and your workflow.
#
# For a single-module app (e.g. one cli.py at the root), replace the hatch
# packages line with a py-modules or only-include setup; or use setuptools.

[build-system]
requires = ["hatchling"]          # Build dependency: the hatchling build backend
build-backend = "hatchling.build" # Use hatchling to build sdists and wheels

[project]
name = "my-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"        # Minimum Python version required
dependencies = []                 # Runtime dependencies (add packages your code imports)

[dependency-groups]
dev = [                           # Development tools (linting, formatting, type checking)
    "ruff>=0.15.7",               # Linter and formatter
    "ty>=0.0.24",                 # Type checker
    "rumdl>=0.1.57",              # Markdown linter
]
test = [                          # Testing tools
    "pytest>=9.0.2",              # Test runner
    "pytest-cov>=7.0.0",          # Coverage reporting plugin for pytest
]
docs = [                          # Documentation tools
    "zensical",                   # Documentation generator
    "mkdocstrings[python]",       # Auto-generate API docs from Python docstrings
]

[tool.uv]
default-groups = ["dev", "test", "docs"]  # Install all dependency groups by default with uv sync

# Hatch wheel build: which packages or modules to include in the built .whl.
#
# — src layout (default): package lives in src/my_project/
#   packages = ["src/my_project"]
#
# — Package at project root (my_project/ next to pyproject.toml):
#   packages = ["my_project"]
#
# — Single or multiple .py modules at project root:
#   only-include = ["cli.py"]
#   only-include = ["mod1.py", "mod2.py"]
#
[tool.hatch.build.targets.wheel]
packages = ["src/my_project"]     # Directory to include when building the wheel (src layout)

# For a CLI app, add (adjust module path and callable):
# [project.scripts]
# my-command = "my_project.cli:main"

# ---------------------------------------------------------------------------
# ruff — formatting and linting
# ---------------------------------------------------------------------------

[tool.ruff]
line-length = 100                 # Max line length for both linting and formatting
target-version = "py312"          # Assume Python 3.12 syntax for linting rules

[tool.ruff.format]
quote-style = "double"            # Use double quotes for strings
indent-style = "space"            # Use spaces, not tabs, for indentation

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors (e.g. whitespace, indentation)
    "F",    # Pyflakes (unused imports, undefined names)
    "I",    # isort (import ordering)
    "N",    # pep8-naming (naming conventions)
    "W",    # pycodestyle warnings
    "UP",   # pyupgrade (modernize syntax for target Python version)
    "B",    # flake8-bugbear (common bug patterns and design problems)
    "C4",   # flake8-comprehensions (simplify list/dict/set comprehensions)
    "SIM",  # flake8-simplify (suggest simpler constructs)
]
ignore = [
    "E501", # Line too long — handled by the formatter instead
]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]          # Allow unused imports in __init__.py (re-exports)

# ---------------------------------------------------------------------------
# ty — type checking
# ---------------------------------------------------------------------------

[tool.ty.environment]
python-version = "3.12"           # Python version to use for type resolution

[tool.ty.src]
exclude = ["tests"]               # Skip type checking the tests directory

[tool.ty.rules]
# Promote warnings to errors as the codebase matures:
# possibly-unbound = "error"      # Flag variables that may be used before assignment
# invalid-return-type = "error"   # Flag functions returning wrong types

[tool.ty.terminal]
output-format = "full"

# ---------------------------------------------------------------------------
# rumdl — Markdown linting
# ---------------------------------------------------------------------------

[tool.rumdl]
flavor = "mkdocs"                 # Parse mkdocs-specific Markdown extensions (admonitions, tabs, etc.)
line-length = 100                 # Max line length for Markdown files
respect-gitignore = true          # Also skip files listed in .gitignore
disable = [                       # Rules to disable globally
    "MD007",                      # Unordered list indentation — too strict for nested lists
    "MD013",                      # Line length — handled at project level or not enforced
    "MD031",                      # Fenced code blocks surrounded by blank lines
    "MD033",                      # Inline HTML — often needed for docs
    "MD046",                      # Code block style consistency
]
exclude = [".git", ".github", "node_modules", "vendor", "dist", "build", "CHANGELOG.md", "LICENSE.md"]  # Directories/files to skip

[tool.rumdl.MD003]
style = "atx"                     # Heading style: use # headings (not underline/setext)

[tool.rumdl.MD004]
style = "dash"                    # Unordered list marker: use dashes (- item)

[tool.rumdl.MD013]
line-length = 100                 # Line length limit for prose in Markdown
code-blocks = false               # Don't enforce line length inside code blocks
tables = false                    # Don't enforce line length inside tables

[tool.rumdl.MD024]
siblings-only = true              # Allow duplicate headings if they're under different parents

[tool.rumdl.MD033]
allowed-elements = ["br", "details", "summary", "img"]  # Inline HTML elements that are OK to use

[tool.rumdl.MD044]
names = ["GitHub", "Python", "Markdown"]  # Proper nouns that must use correct capitalization
code-blocks = false               # Don't enforce capitalization inside code blocks

# ---------------------------------------------------------------------------
# pytest
# ---------------------------------------------------------------------------

[tool.pytest.ini_options]
testpaths = ["tests"]             # Directory where pytest looks for test files
addopts = [
    "-v",                         # Verbose output: show individual test names
    "--tb=short",                 # Use short tracebacks on failure
    "--strict-markers",           # Fail on unknown @pytest.mark decorators
    "--strict-config",            # Fail on unrecognized config options
]
← Back to all posts