Configuring tools using pyproject.toml
•
Pythonrufftyrumdlpytest
My Standard pyproject.toml Setup
Keeping a Python project clean and consistent requires the right tooling. I consolidate all my configurations into pyproject.toml to keep the root directory tidy. Here is the breakdown of the settings I use for testing, linting, and documentation.
1. Ruff (Linting & Formatting)
Ruff has replaced almost all my previous linting tools (Flake8, Isort, Black). I use a broad selection of rules but ignore specific "noisy" ones like magic values in tests or line-length errors (which the formatter handles).
[tool.ruff]
line-length = 100
target-version = "py312"
exclude = [
".git",
".venv",
"venv",
"__pycache__",
".pytest_cache",
".ruff_cache",
"build",
"dist",
"*.egg-info",
"htmlcov",
".cache",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"W", # pycodestyle warnings
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"A", # flake8-builtins
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"PIE", # flake8-pie
"PT", # flake8-pytest-style
"RSE", # flake8-raise
"RET", # flake8-return
"SIM", # flake8-simplify
"PTH", # flake8-use-pathlib
"PL", # pylint
"PERF", # perflint
"RUF", # ruff-specific rules
]
ignore = [
"E501", # Line too long (handled by formatter)
"PLR2004", # Magic value in comparison (too noisy, especially in tests)
"PLR0911", # Too many return statements
"PLR0912", # Too many branches
"PLR0913", # Too many arguments
"PLR0915", # Too many statements
"PTH", # Use pathlib (prefer gradual migration, not enforced)
"RET504", # Unnecessary assignment before return
"RET505", # Unnecessary else after return
"DTZ005", # datetime.now() without timezone (not all apps need timezone-aware)
"SIM105", # Use contextlib.suppress (try/except is often clearer)
"SIM102", # Collapsible if (sometimes separate ifs are clearer)
"SIM117", # Use single with statement (nested is often clearer)
"PLW1510", # subprocess.run without check (handled case-by-case)
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
"PLC0415", # Import outside top-level (tests often need scoped imports)
"PLR2004", # Magic values (test assertions with status codes, etc.)
"F841", # Unused variable (fixtures, etc.)
"PT001", # Use @pytest.fixture() over @pytest.fixture
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
2. Type checking
I use Ty for type checking.
# Ty Environment
[tool.ty.environment]
python-version = "3.12"
[tool.ty.src]
exclude = ["tests"]
3. Rumdl (Markdown Linting)
I use Rumdl for Markdown linting.
[tool.rumdl]
flavor = "mkdocs"
respect-gitignore = true
exclude = [".git", ".github", "node_modules", "vendor", "dist", "build", "CHANGELOG.md", "LICENSE.md"]
disable = [
"MD007" # Unordered list indentation: This rule usually enforces a specific number of spaces (often 2) for nested list items. Disabling it allows you to be more flexible with how you indent your bullet points.
"MD013", # Line length: Allows lines longer than the default limit for better readability in technical docs
"MD031", # Fenced code blocks should be surrounded by blank lines: Requires a blank line before and after code blocks. Disabling this allows code blocks to sit immediately adjacent to text or other elements.
"MD033", # Inline HTML: Permits HTML tags within markdown for enhanced formatting when needed
"MD046", # Code block style: Checks for consistency between indented code blocks and fenced (backtick) code blocks. Disabling this allows you to mix both styles in one document.
]
4. Pytest
I prefer strict configurations to ensure tests are predictable and easy to find.
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = ["--strict-markers", "--strict-config", "--tb=short", "--verbose"]
5. Coverage
To keep my reports meaningful, I exclude migrations, virtual environments, and boilerplate code like __repr__ or type-checking blocks.
[tool.coverage.run]
source = ["blueprints", "services", "database"]
omit = ["tests/*", "*/__pycache__/*", "*/migrations/*", "venv/*", ".venv/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
show_missing = true
precision = 2
[tool.coverage.html]
directory = "htmlcov"