Skip to main content

Report mypy annotation coverage for a Python codebase.

Project description

mypy-coverage

A fast, stdlib-only CLI that reports how much of a Python codebase is actually type-checked by mypy.

The catch with mypy's default check_untyped_defs = False is that fully unannotated functions are silently skipped — their bodies are not analysed and any real type errors inside them are invisible. mypy-coverage enumerates exactly these, plus files covered by the exclude pattern, and computes aggregate coverage percentages.

Install

Latest tagged release (recommended):

pip install "git+https://github.com/mfisherlevine/mypy_coverage.git@v0.1"

Latest released code on main:

pip install "git+https://github.com/mfisherlevine/mypy_coverage.git"

Bleeding edge (dev branch):

pip install "git+https://github.com/mfisherlevine/mypy_coverage.git@dev"

For local development:

git clone https://github.com/mfisherlevine/mypy_coverage.git
cd mypy_coverage
pip install -e '.[dev]'
pre-commit install
pytest

Requires Python 3.11+. The runtime has zero third-party dependencies; the dev extra pulls in pytest, mypy, ruff, and pre-commit. Installing provides a mypy-coverage console script and a mypy_coverage importable package (also runnable via python -m mypy_coverage).

Usage

# Scan current project (auto-detect config)
mypy-coverage

# Scan specific paths
mypy-coverage src/ tests/

# Fail CI if body-checked coverage is below 85%
mypy-coverage --threshold 85

# Full list of every unannotated definition
mypy-coverage --list

# List partially annotated ones too
mypy-coverage --list --list-partial

# Machine-readable output
mypy-coverage --format json

# GitHub Actions annotations
mypy-coverage --format github

# Flag imports that decay to Any
mypy-coverage --silent-any

# Or use python -m
python -m mypy_coverage --help

The package also exposes a small programmatic API:

from pathlib import Path
from mypy_coverage import build_report, discover_config, load_config

cfg_path = discover_config(Path.cwd())
cfg = load_config(cfg_path) if cfg_path else None
report = build_report([Path("src")], cfg, root=Path.cwd())
print(f"{report.percent_checked():.1f}% body-checked")
for d in report.definitions:
    if d.status == "unannotated":
        print(d.file, d.lineno, d.qualname)

What counts as "covered"?

Each function, method, or class falls into one of four buckets:

Status Meaning
annotated Every param (excluding self/cls) and the return type are annotated.
partial At least one annotation. Mypy does check the body; missing types become Any.
unannotated Zero annotations. Mypy skips the body when check_untyped_defs = False.
excluded File matches the mypy exclude regex; mypy never sees it.

Two coverage metrics are reported:

  • body-checked by mypy = (annotated + partial) / (total - excluded) — the fraction of definitions whose bodies mypy analyses.
  • fully annotated = annotated / (total - excluded) — stricter; what you'd get under disallow_untyped_defs.

Config discovery

Walks up from the current directory looking for, in order:

  1. mypy.ini / .mypy.ini
  2. setup.cfg (with a [mypy] section)
  3. pyproject.toml (with a [tool.mypy] table)

The tool reads:

  • check_untyped_defs — affects how unannotated bodies are treated
  • exclude — regex of paths mypy skips
  • files and mypy_path — default set of paths to scan
  • ignore_missing_imports per-module — powers --silent-any

If no config is found, the current directory is scanned with mypy defaults.

Silent-Any detection (--silent-any)

A best-effort scan for syntactic patterns that usually decay to Any:

  • ignored-import — a symbol imported from a module configured with ignore_missing_imports = True. Everything that symbol names is Any.
  • untyped-decorator — a function decorated with a name imported from an ignored module. The decorator can silently erase the wrapped function's return type.
  • type-ignore — any # type: ignore comment.

True "silent Any" detection (types that collapse to Any during mypy's semantic analysis) requires actually running mypy; see the roadmap note.

Output formats

  • text (default) — human-readable summary, per-file table, optional listings
  • json — complete machine-readable dump including per-definition records
  • markdown — suitable for pasting in PRs and issues
  • github::warning / ::notice annotations for GitHub Actions

CI use

- name: mypy coverage
  run: |
    pip install mypy-coverage
    mypy-coverage --threshold 85 --format github

Exit codes:

  • 0 — scan succeeded and, if --threshold was given, coverage met it
  • 1 — coverage below threshold
  • 2 — invalid arguments or missing config/paths

Development

pip install -e '.[dev]'
pre-commit install   # install the git hook
pytest               # ~120 unit/integration tests
mypy                 # strict; package and tests should both be clean
mypy-coverage        # dogfood: should report 100% coverage of itself

Pre-commit runs ruff (lint + format), mypy, and a handful of hygiene hooks (trailing whitespace, EOF newline, large files, merge conflicts, debug statements). GitHub Actions CI (.github/workflows/ci.yml) runs the same pre-commit chain plus pytest on Python 3.11/3.12/3.13 and a self- coverage check that must be exactly 100%. These are required status checks before merging to main.

Package layout:

src/mypy_coverage/
  models.py        Dataclasses and status constants
  config.py        mypy.ini / setup.cfg / pyproject.toml parsing
  discovery.py     File walking, exclude-regex matching
  scanner.py       AST walk, function/method/class classification
  silent_any.py    --silent-any detection
  report.py        build_report + per-file aggregation
  render.py        text / json / markdown / github renderers
  cli.py           argparse and main entry point

Limitations

  • The scan is syntactic. It does not resolve imports, so a function with an annotation that references an unresolvable name is still counted as annotated. Running mypy itself is the only way to catch that.
  • --silent-any is heuristic. It won't catch every path to Any — in particular, Any introduced by calling an untyped function returning Any is invisible without running mypy.
  • Possible future flag --deep: shell out to mypy --disallow-any-unimported --disallow-any-decorated and merge the diagnostics for a more thorough silent-Any check.

License

GPL-3.0-or-later. See LICENSE. Chosen to match the wider Rubin Observatory / LSST Pipelines ecosystem from which this tool originated.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mypy_coverage-0.1.0.tar.gz (27.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mypy_coverage-0.1.0-py3-none-any.whl (31.5 kB view details)

Uploaded Python 3

File details

Details for the file mypy_coverage-0.1.0.tar.gz.

File metadata

  • Download URL: mypy_coverage-0.1.0.tar.gz
  • Upload date:
  • Size: 27.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for mypy_coverage-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b99a51e20237f43239362a519df9d097b39b5aa48a7852f835cfea1c3726480f
MD5 78da95a522d945b7d8c7b508820e2dfd
BLAKE2b-256 e90f6ae9f1a9a6ad316273754a249f7d51cef0744b235ad2e7b03af7d258bf87

See more details on using hashes here.

File details

Details for the file mypy_coverage-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: mypy_coverage-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for mypy_coverage-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cfb2136ca0bef964207869bdc3aa1c90920b421510be3be1b0af272dbf925d3c
MD5 6d7c7ae60182b6622c8a129c124411b5
BLAKE2b-256 0ca34312038b889967494217c143650ce51b29321bb089a24168a6ba0f61a22d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page