← Back to Blog

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"
← Back to all posts