Skip to Content
Devopspyproject.toml Tooling — Black, isort, mypy, pytest, Coverage

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 = 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

  • 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 = true for tests.* — 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.something instead 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 into htmlcov/ 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

  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:", ]
  • 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 = true
  • warn_return_any — flags any function returning Any (workaround for libraries without type hints).
  • no_implicit_optional — explicit x: int | None style 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.

Last updated on