Skip to Content

pyproject.toml Tooling — Black, isort, mypy, pytest, Coverage

Data Flow

PYPROJECT.TOML TOOLING  —  Data Flow

   pyproject.toml

        ├─── [tool.black]               ── line-length 100, target py311   (pre-commit)
        ├─── [tool.isort]               ── profile=black, multi_line_output=3
        ├─── [tool.mypy]                ── follows imports, ignores missing, warn_unused
        ├─── [tool.pytest.ini_options]  ── asyncio mode, --cov=app, --cov-report term+html
        ├─── [tool.coverage.run]        ── omit tests/, migrations/
        └─── [tool.coverage.report]     ── exclude pragma, Protocol lines


   developers: pre-commit hooks   (black, isort, flake8)
   CI:         pytest, mypy, coverage gate

What? (Concept Overview)

pyproject.toml is the single source of truth for Python build metadata AND developer-tool configuration. PEP 621 governs the [project] block; PEP 518 governs [build-system]. Tool-specific blocks ([tool.black], [tool.pytest.ini_options], etc.) live in the same file so a git clone reproduces lint, type-check, test, and coverage behaviour with zero auxiliary config.

Project Context

The FCA Support Agent’s pyproject.toml lays out the contracts the team relies on:

How? (Quick Reference Blocks)

3.1 Build System & Project Metadata

# pyproject.toml
[build-system]
requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
 
[project]
name = "fca-multi-agent-support"
version = "0.1.0"
description = "FCA-compliant multi-agent AI support system for UK financial services"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [{name = "David Sandeep", email = "..."}]
keywords = ["ai", "multi-agent", "fca", "compliance", "support"]
classifiers = [
  "Development Status :: 3 - Alpha",
  "Framework :: FastAPI",
]

3.2 Black & isort

# pyproject.toml
[tool.black]
line-length = 100
target-version = ['py311']
include = '\.pyi?$'
extend-exclude = '''/(\.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv
  | build | dist)/'''
 
[tool.isort]
profile = "black"           # harmonise with Black's import grouping
line_length = 100
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true

3.3 mypy Configuration

# pyproject.toml
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false            # onboarding-friendly
disallow_incomplete_defs = false
check_untyped_defs = true               # CHEC类型 for untyped fns
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
follow_imports = "normal"
ignore_missing_imports = true           # tolerate third-party stubs
 
[[tool.mypy.overrides]]
module = ["tests.*"]
ignore_errors = true                    # tests get a pass on type errors

3.4 pytest & Coverage

# pyproject.toml
[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = """
    -v
    --strict-markers
    --strict-config
    --cov=app
    --cov-report=term-missing
    --cov-report=html
"""
markers = [
  "asyncio: marks tests as async (deselect with '-m \"not asyncio\"')",
  "integration: marks tests as integration tests",
  "unit: marks tests as unit tests",
]
 
[tool.coverage.run]
source = ["app"]
omit = ["*/tests/*", "*/migrations/*", "*/__init__.py"]
 
[tool.coverage.report]
exclude_lines = [
  "pragma: no cover",
  "def __repr__",
  "raise AssertionError",
  "raise NotImplementedError",
  "if __name__ == .__main__.:",
  "if TYPE_CHECKING:",
  "class .*\\bProtocol\\):",
  "@(abc\\.)?abstractmethod",
]

Why? (Parameter Breakdown

Common Pitfalls

  1. Two config files fighting — Older projects have setup.cfg, .flake8, mypy.ini, and pyproject.toml. Migrate ALL tool config into pyproject.toml first; legacy files take precedence silently.
  2. disallow_untyped_defs = true on day one — Even senior teams hit friction on bulky class hierarchies (__init__, __repr__, Pydantic models). Don’t enforce until codebase is typed end-to-end.

Real-World Interview Prep

Q1: How would you evolve disallow_untyped_defs = false to true incrementally?

A: Add per-file overrides. Start with disallow_untyped_defs = true for app/services/ (highest value, easiest to type), set disallow_untyped_defs = false globally. Use an [[tool.mypy.overrides]] block to expand the strict module list over time. Track the count of untyped defs in mypy --strict-style reports and gate on it via CI: --warn-unused-ignores + a custom script that fails PRs introducing new untyped defs in protected modules.

Q2: A new contributor opens a PR; their pytest works locally but the project’s CI marks it. Why?

A: Three likely answers. (1) They didn’t run --cov because they excluded it locally; CI runs it as part of addopts → coverage < threshold fails. (2) They wrote @pytest.mark.slow without declaring slow in markers = [...]; --strict-markers fails. (3) They used a Python version > target-version = ['py311']. Standard fix: provide a Makefile / tox.ini target that mirrors CI: make ci → install + lint + type-check + test in one shot.

Q3: Coverage drops from 78% → 74% after a feature. Is that a real regression?

A: Investigate before celebrating. (1) Run diff-cover against the PR branch — which LINES dropped? If they’re in untested admin routes you don’t care about, it’s not a real regression. (2) Coverage-percent alone misleads: removing 50 LOC of easily-covered code can drop % faster than adding complex logic. Switch to coverage report --fail-under=80 --skip-covered. (3) Add tests proportional to the new feature’s risk surface, not its LOC.

Top-to-Bottom Code Walkthrough (pyproject.toml)

pyproject.toml is the modern single source of truth for Python project metadata, dependencies, and tool configuration. It replaces setup.py, setup.cfg, and per-tool config files.

[build-system]

[build-system]
requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"

Tells pip install . which build system to use. setuptools is the default.

[project]

[project]
name = "fca-multi-agent-support"
version = "0.1.0"
description = "FCA-compliant multi-agent AI support system for UK financial services"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [{name = "David Sandeep", email = "davidsandeep1996@gmail.com"}]
classifiers = [
  "Programming Language :: Python :: 3.11",
  "Framework :: FastAPI",
]

PEP 621 metadata — what’s published to PyPI if you ever pip install fca-multi-agent-support. The requires-python = ">=3.11" makes pip reject incompatible Python versions early.

[tool.black]

[tool.black]
line-length = 100
target-version = ['py311']
extend-exclude = '''/(...)/'''

Black is the formatter. line-length = 100 (a touch wider than Black’s default 88 for readability). target-version = ['py311'] lets Black use Python 3.11 syntax.

[tool.isort]

[tool.isort]
profile = "black"
line_length = 100
multi_line_output = 3
include_trailing_comma = true

profile = "black" makes isort agree with Black’s style — no more fights between the two formatters. multi_line_output = 3 (vertical hanging indent) is the most readable.

[tool.pytest.ini_options]

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = """
    -v --strict-markers --strict-config
    --cov=app --cov-report=term-missing --cov-report=html
"""
markers = [
  "asyncio: marks tests as async",
  "integration: marks tests as integration tests",
  "unit: marks tests as unit tests",
]

--strict-markers is critical — typos like @pytest.mark.intgeration fail instead of running silently. --cov=app measures coverage of the app directory.

[tool.coverage.run] and [tool.coverage.report]

[tool.coverage.run]
source = ["app"]
omit = ["*/tests/*", "*/migrations/*", "*/__init__.py"]
 
[tool.coverage.report]
exclude_lines = [
  "pragma: no cover",
  "raise NotImplementedError",
  "if __name__ == .__main__.:",
  "if TYPE_CHECKING:",
]

[tool.mypy]

[tool.mypy]
python_version = "3.11"
warn_return_any = true
disallow_incomplete_defs = false
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true

Common Pitfalls

Forgetting markers = [...] with new marker names — pytest rejects unknown markers when --strict-markers is on.

Conflicting Black and isort configs — both want to format imports. profile = "black" is the harmoniser.

Setting disallow_untyped_defs = true on FastAPI starts an uprising because FastAPI uses Pydantic internals without type hints. The team’s compromise is disallow_untyped_defs = false; check_untyped_defs = true.

Real-World Interview Prep

Q1: Why use pyproject.toml over multiple config files?

A: One place to look. With per-tool config files you’d have setup.cfg, .black, .isort.cfg, pytest.ini, .mypy.ini, .flake8 — 6+ files of config. pyproject.toml consolidates into a single TOML that’s PEP-standard.

Q2: What’s setuptools-scm[toml]?

A: Auto-generates the version from the git tag. git tag v1.2.3 → project reports 1.2.3 without manual editing.

Q3: Why python = ">=3.11" specifically?

A: Project uses StrEnum, tomllib, and Self — features that need 3.11+. Without the constraint, pip installs on 3.9 fail at first import.

🛠️ Production Failure Playbook

🚨 Failure 1 — Black line-length mismatch → CI fails on whitespace

🚨 Failure 2 — mypy strict mode off → type regressions ship

🚨 Failure 3 — Coverage omit patterns misconfigured → false high coverage

Advertisement