packages/shared-skills/skills/programming/references/python/pyproject-strict.md
The canonical "super strict but sane" config for modern Python projects. Copy-paste, then add your own dependencies.
# Application
uv init --app myproject
cd myproject
# Library (publishable to PyPI)
uv init --lib mylibrary
cd mylibrary
# Add dev tools
uv add --dev basedpyright ruff pytest
uv init creates pyproject.toml, .python-version, and src/ layout. Replace its pyproject.toml [tool.*] sections with the block below.
[project]
name = "myproject"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[dependency-groups]
dev = [
"basedpyright>=1.21",
"ruff>=0.8",
"pytest>=8",
"pytest-cov>=5",
]
# ─────────────────────────────────────────────────────────────────
# basedpyright - typeCheckingMode = "all" sets every report flag to error
# Source: https://docs.basedpyright.com/latest/configuration/config-files/
# ─────────────────────────────────────────────────────────────────
[tool.basedpyright]
typeCheckingMode = "all"
pythonVersion = "3.13"
pythonPlatform = "All" # default in basedpyright; explicit for clarity
include = ["src", "tests"]
exclude = ["**/__pycache__", "**/.venv", "**/build", "**/dist"]
# Strict enforcement extras (most are already "error" under "all" mode,
# but listing them explicitly documents the intent)
reportUnusedCallResult = "warning" # flag ignored return values
reportUnnecessaryTypeIgnoreComment = "error" # stale type: ignore comments must die
reportUnusedVariable = "error" # unused variables are errors
reportMissingParameterType = "error" # every parameter must have a type
reportMissingReturnType = "error" # every function must declare its return type
reportPrivateUsage = "error" # respect _private convention
# Optional: gradual adoption baseline
# baselineFile = "./.basedpyright/baseline.json"
# ─────────────────────────────────────────────────────────────────
# ruff - select = ["ALL"] enables every rule, then we ignore the
# small set that conflicts with the formatter or is not useful.
# Source: https://docs.astral.sh/ruff/linter/#rule-selection
# ─────────────────────────────────────────────────────────────────
[tool.ruff]
target-version = "py313"
line-length = 88 # ruff/black default; 100 or 120 also fine
src = ["src", "tests"]
[tool.ruff.lint]
select = ["ALL"]
ignore = [
# Formatter conflicts (ruff itself tells you to ignore these)
"COM812", # missing trailing comma
"ISC001", # implicit string concat
# Docstyle conflicts (pick D211 over D203, D212 over D213)
"D203",
"D213",
# Project-specific noise
"CPY001", # missing copyright notice
"FBT001", # boolean positional arg in def
"FBT002", # boolean positional default in def
"TD002", # missing TODO author
"TD003", # missing TODO link
"FIX002", # line contains TODO (TODOs are allowed)
]
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
"S101", # `assert` is the entire point of pytest
"ARG", # unused args (fixtures appear unused)
"PLR2004", # magic numbers in test data
"SLF001", # tests need access to private members
"D", # docstrings not required in tests
]
"scripts/**/*.py" = [
"T201", # `print` allowed in scripts
"INP001", # implicit namespace package
]
[tool.ruff.lint.pydocstyle]
convention = "google" # or "numpy" / "pep257"
[tool.ruff.lint.flake8-bugbear]
# typer / fastapi rely on call-as-default for parameter metadata.
# Without this, ruff B008 ("function call in default") fires on every typer/fastapi route.
extend-immutable-calls = [
"typer.Argument",
"typer.Option",
"fastapi.Depends",
"fastapi.Query",
"fastapi.Path",
"fastapi.Body",
"fastapi.Header",
"fastapi.Cookie",
"fastapi.File",
"fastapi.Form",
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
docstring-code-line-length = "dynamic"
# ─────────────────────────────────────────────────────────────────
# pytest
# ─────────────────────────────────────────────────────────────────
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = [
"-ra",
"--strict-config",
"--strict-markers",
]
filterwarnings = ["error"]
# ─────────────────────────────────────────────────────────────────
# coverage
# ─────────────────────────────────────────────────────────────────
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
"raise NotImplementedError",
"@(abc\\.)?abstractmethod",
]
typeCheckingMode = "all"basedpyright's modes, strictest first:
| Mode | Behavior |
|---|---|
"all" | Every diagnostic at error |
"recommended" | Same rules; less severe ones at warning; failOnWarnings = true makes CI still fail |
"strict" | pyright's strict mode |
"standard" | Default |
"basic" / "off" | Loose / disabled |
"all" enables basedpyright-exclusive rules pyright lacks: reportImplicitOverride, reportImplicitStringConcatenation, reportIncompatibleUnannotatedOverride, reportUnannotatedClassAttribute. No need to opt-in to additional flags.
pythonPlatform = "All" is basedpyright's default (better than pyright's host-OS default) - it errors on platform-specific imports that fail on other OSes.
select = ["ALL"]The official docs say "Use ALL with discretion. Enabling ALL will implicitly enable new rules whenever you upgrade." For a strict skill that is the intended behavior - every new ruff rule should be considered an error until you justify ignoring it.
The minimal ignore set:
| Rule | Reason |
|---|---|
COM812, ISC001 | Conflict with ruff format (ruff itself documents this) |
D203 vs D211, D213 vs D212 | Mutually-exclusive docstring conventions; pick the modern one |
CPY001 | Most projects don't need a copyright header on every file |
FBT001, FBT002 | Boolean flags are ergonomic for CLI/typer; ban makes typer awkward |
TD002, TD003, FIX002 | TODOs without a JIRA link are fine in solo / internal code |
ANN101 and ANN102 were removed in ruff 0.8.0 (Nov 2024). Do NOT include them in ignore - ruff errors on unknown rule codes.
per-file-ignores for tests/** is the standard pattern from real-world repos like community-of-python/auto-typing-final and Preston-Landers/concurrent-log-handler.
# In CI, fail on any violation:
uv run basedpyright
uv run ruff check
uv run ruff format --check
uv run pytest
A single make ci target combining the four works fine.
The config above, combined with scripts/check-no-excuse-rules.py, enforces:
| What | How |
|---|---|
| Exhaustive match | basedpyright all mode + assert_never |
No Any | basedpyright all mode + script cast-any rule |
| Ignored return values | reportUnusedCallResult = "warning" |
| Immutable default | Script mutable-dataclass + missing-slots rules |
| No null surprise | basedpyright strict None analysis |
| Constants are const | basedpyright catches Final reassignment |
| Unused variables | reportUnusedVariable = "error" |
"all" vs "recommended": https://docs.basedpyright.com/latest/configuration/config-files/#recommended-and-all