pyproject.toml Tooling — Black, isort, mypy, pytest, Coverage
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:
- Black formats at line-length 100 (slightly wider than the default 88, suits domain-rich docs and long identifiers)
- isort is configured for “black” profile to prevent Black-vs-isort ordering fights
- mypy runs strict type-checking but exempts
tests/* - pytest auto-marks tests with
asyncio, integrates coverage by default, and writes both terminal missing-line and HTML reports - Coverage excludes standard junk (
pragma: no cover,__repr__, abstract methods, type-checking branches)
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 = true3.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 errors3.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
line-length = 100— Industry compromise: wider than Black’s default 88 to fit domain-rich identifier names (agent_coordinator_url_sync), tighter than 120 so log lines stay readable.profile = "black"for isort — Forces isort to import-group EXACTLY like Black — no more “isort moved my import” battles. Without it, you waste minutes per file on merge conflicts.warn_return_any = true,disallow_untyped_defs = false— Pragmatic tension: STRICT detection of unsafe returns, but allow untyped defs so junior engineers aren’t blocked. Tightens over time as the codebase matures.ignore_errors = truefortests.*— Tests emphasize behaviour over types; type-checking them adds friction without proportional value. Exemption is conventional.--strict-markers&--strict-config— Errors out on an undeclared@pytest.mark.somethinginstead of silently passing. Critical for catching typos in CI.--cov=app+--cov-report=htm— Coverage runs as part of every pytest invocation; the HTML report goes intohtmlcov/for PR diff inspection. Coverage as a side effect, not a separate command.[tool.coverage.report].exclude_lines— Standard exclusions: dunder methods, abstract methods, type-checking blocks. Reduces noise so coverage delta gates signal real regressions.
Common Pitfalls
- Two config files fighting — Older projects have
setup.cfg,.flake8,mypy.ini, andpyproject.toml. Migrate ALL tool config intopyproject.tomlfirst; legacy files take precedence silently. disallow_untyped_defs = trueon 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 = trueprofile = "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:",
]source = ["app"]— only measure packages within app/.exclude_lines— tells coverage to skip abstract methods, debug__main__blocks, type-checking imports.
[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 = truewarn_return_any— flags any function returningAny(workaround for libraries without type hints).no_implicit_optional— explicitx: int | Nonestyle preferred.[[tool.mypy.overrides]] module = ["tests.*"]— loosens type-checking for tests.
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.