# Share a mypy HTML Report — Type Coverage Someone Will Actually Read

Canonical: https://commareports.com/share-mypy-report
Published: 2026-09-14

> mypy --html-report writes a per-module type-coverage site nobody opens. Publish it to Comma from CI: one URL, a revision per run, and comments on the module worth annotating next.

# Share a mypy HTML report

Adopting typing in an existing Python codebase is a long grind with one
recurring question: what do we annotate next? The answer is in a report almost
nobody generates, because the report has nowhere to go.

```bash
pip install 'mypy[reports]'
mypy --html-report mypyreport/ src/
```

That writes a ranked table of every module by _imprecision_ — the share of
lines mypy could not check precisely — and a page per module with the offending
lines highlighted. It is the closest thing to a prioritised worklist the type
migration will ever have, and it lands in a directory that gets `.gitignore`d.

## Precision, not annotation count

The number is worth understanding before anyone argues about it. A module can
be fully annotated and still score badly, because a dependency returns `Any`
and that `Any` spreads through everything it touches. That is the useful
signal: it points at the one stub package worth installing rather than at the
hundred functions worth annotating.

Which is also why this should be a link and not a percentage in a CI log. The
percentage starts an argument. The page ends it.

## Publish the folder

Drag `mypyreport/` (or a zip of it) into
[Comma](https://commareports.com/):

- `index.html` becomes the **report body** — the ranked table.
- The per-module pages and stylesheet upload alongside it, with relative
  references rewritten to the uploaded copies, so clicking a module row still
  opens its annotated source.
- Scripts run inside a sandboxed iframe (`allow-scripts`, no
  `allow-same-origin`).

## From CI

```bash
mypy --html-report mypyreport/ src/ || true   # report even when the gate fails

curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html mypyreport/index.html \
        --arg t "Type coverage — $(git rev-parse --short HEAD)" \
        '{title: $t, html: $html}')"
```

The `|| true` matters. A type-coverage report is most interesting on the run
that failed, and a job that exits before publishing throws away the artifact
that explains why. See [publishing from CI](/docs/ci) and the
[API reference](/docs/api).

Supporting pages go up through `POST /api/v1/reports/$REPORT_ID/assets` as
base64.

## A revision per run is the migration record

PATCHing one saved report id keeps a permanent URL, and every publish appends a
revision. Six months of those is the only honest answer to
"is the typing effort actually working?" — better than a graph, because you can
open any two points and see which modules moved.

## Comments, so a decision stays decided

Half the red rows on a type-coverage report are red on purpose: a legacy module
nobody will annotate, a dynamic dispatch layer that resists typing, a
dependency without stubs. Anchoring that reasoning to the row means the next
engineer reads it instead of rediscovering it. See
[commenting on HTML](/comment-on-html).

## Limits

- **Entry HTML: 5 MB.** Assets: 25 MB per file, **250 MB and 500 files total**.
  A large codebase generates one page per module — over 500, publish the index
  alone, or report per package.
- **Scripts run, sandboxed** — no same-origin access.
- **60 requests/minute per token.**

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision
history.

**[Publish a mypy report →](https://commareports.com/)**

### Related

- [Share a flake8 report](/share-flake8-report) · [Share a Ruff report](/share-ruff-report)
- [Share a pylint report](/share-pylint-report) · [Share a pytest HTML report](/share-pytest-report)
- [Share a coverage report](/share-coverage-report) · [Publishing from CI](/docs/ci)
