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
From PyPI (recommended):
pip install mypy-coverage
Or in an isolated environment with pipx:
pipx install mypy-coverage
Alternative install channels
Pinned git tag:
pip install "git+https://github.com/mfisherlevine/mypy_coverage.git@v0.2.3"
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)
Sample report
The default text format prints a colourised summary to the terminal:
--format markdown --list produces the same data laid out for PR
comments, GitHub issues, or this README:
mypy-coverage report
❌ 17 unannotated, 2 partial definition(s).
- Root:
~/projects/example- Config:
~/projects/example/pyproject.toml- Files scanned: 42
- Files excluded: 3
Summary
metric value ❌ body-checked by mypy 87.2% ❌ fully annotated 83.9% annotated 220 partial 2 unannotated 17 Files with gaps
file fully typed % body checked % unannotated partial src/example/legacy/parser.py33.3% 66.7% 2 2 src/example/io/loader.py50.0% 50.0% 6 0 src/example/utils/text.py71.4% 71.4% 2 0 src/example/cli/commands.py77.8% 77.8% 2 0 Unannotated definitions (17)
src/example/legacy/parser.py
- L25
parse_token(function)- L29
parse_block(function)
src/example/io/loader.py
- L4
read(function)- L8
_open(function)- L12
iter_records(function)- L16
load_async(function)- L21
Loader.add(method)- L24
Loader.flush(method)- …
The same data is also available as --format json (machine-readable),
--format text (terminal-friendly with optional ANSI colour), and
--format github (::warning / ::notice annotations on the PR diff).
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 underdisallow_untyped_defs.
Config discovery
Walks up from the current directory looking for, in order:
mypy.ini/.mypy.inisetup.cfg(with a[mypy]section)pyproject.toml(with a[tool.mypy]table)
The tool reads:
check_untyped_defs— affects how unannotated bodies are treatedexclude— regex of paths mypy skipsfilesandmypy_path— default set of paths to scanignore_missing_importsper-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 withignore_missing_imports = True. Everything that symbol names isAny.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: ignorecomment.
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 listingsjson— complete machine-readable dump including per-definition recordsmarkdown— suitable for pasting in PRs and issuesgithub—::warning/::noticeannotations for GitHub Actions
CI use
As a GitHub Action
The repo ships a composite action so a single uses: line drops
mypy-coverage into any workflow:
- uses: mfisherlevine/mypy_coverage@v0.2.3
with:
threshold: 85
format: github # ::warning / ::notice annotations on the PR diff
Inputs (all optional):
| Input | Default | Description |
|---|---|---|
version |
latest |
Pin the published mypy-coverage version, e.g. 0.2.3. |
python-version |
3.11 |
Python interpreter for the install step. |
paths |
(config default) | Space-separated paths to scan. |
config |
auto-detect | Explicit mypy config file. |
root |
config dir | Project root passed to --root. |
threshold |
none | Fail the step if coverage < this percent. |
threshold-metric |
checked |
checked or fully-typed. |
format |
github |
text / json / markdown / github. |
sort |
path |
path or coverage. |
silent-any |
false |
Set true to enable silent-Any detection. |
include-excluded |
true |
Show the walled-off excluded section. |
list |
false |
List every unannotated definition. |
list-partial |
false |
List every partial definition. |
Outputs:
| Output | Example | Notes |
|---|---|---|
percent-checked |
87.2 |
Body-checked-by-mypy fraction, as a percent. |
percent-fully-typed |
83.9 |
Stricter "everything typed" fraction. |
unannotated |
158 |
Count of unannotated defs in the main body. |
partial |
41 |
Count of partials in the main body. |
total |
1238 |
Total defs in the main body. |
Outputs are set even when the threshold gate fails, so a follow-up step can post a sticky PR comment with the numbers regardless.
As a manual pip install step
If you'd rather not use the composite action:
- name: mypy coverage
run: |
pip install mypy-coverage
mypy-coverage --threshold 85 --format github
Exit codes:
0— scan succeeded and, if--thresholdwas given, coverage met it1— coverage below threshold2— 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-anyis heuristic. It won't catch every path toAny— in particular,Anyintroduced by calling an untyped function returningAnyis invisible without running mypy.- Possible future flag
--deep: shell out tomypy --disallow-any-unimported --disallow-any-decoratedand 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mypy_coverage-0.2.3.tar.gz.
File metadata
- Download URL: mypy_coverage-0.2.3.tar.gz
- Upload date:
- Size: 31.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94ada086a0a6dcebf52e5d2606c7ad71ed961d480093ddc42f56801975f3268d
|
|
| MD5 |
b14d045d4b9f1ec3d4f6dfe66fddbea4
|
|
| BLAKE2b-256 |
0062fb10cba7e0da8afd66aa6ea758c710fb446ba37a2765145ea33df3382d62
|
Provenance
The following attestation bundles were made for mypy_coverage-0.2.3.tar.gz:
Publisher:
release.yml on mfisherlevine/mypy_coverage
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mypy_coverage-0.2.3.tar.gz -
Subject digest:
94ada086a0a6dcebf52e5d2606c7ad71ed961d480093ddc42f56801975f3268d - Sigstore transparency entry: 1382741994
- Sigstore integration time:
-
Permalink:
mfisherlevine/mypy_coverage@10323058b1b2439b4227d7d801052c7926021b2a -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/mfisherlevine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10323058b1b2439b4227d7d801052c7926021b2a -
Trigger Event:
push
-
Statement type:
File details
Details for the file mypy_coverage-0.2.3-py3-none-any.whl.
File metadata
- Download URL: mypy_coverage-0.2.3-py3-none-any.whl
- Upload date:
- Size: 35.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de954d0f44b38da2118c85c9b5d3df49f7d26463d4d2072489a845edda3d79ee
|
|
| MD5 |
dc8a0b17dea68743558b0ad914dd4040
|
|
| BLAKE2b-256 |
42bebb0cc68656f088c964798b1fa4f105fa52a3f042d0db6dca7f4fda9861fc
|
Provenance
The following attestation bundles were made for mypy_coverage-0.2.3-py3-none-any.whl:
Publisher:
release.yml on mfisherlevine/mypy_coverage
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mypy_coverage-0.2.3-py3-none-any.whl -
Subject digest:
de954d0f44b38da2118c85c9b5d3df49f7d26463d4d2072489a845edda3d79ee - Sigstore transparency entry: 1382741997
- Sigstore integration time:
-
Permalink:
mfisherlevine/mypy_coverage@10323058b1b2439b4227d7d801052c7926021b2a -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/mfisherlevine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10323058b1b2439b4227d7d801052c7926021b2a -
Trigger Event:
push
-
Statement type: