Skip to content

← Structural Typing and the Hard Parts step 21 of 24

Easy Framework

Suppression hygiene: planning the minimum set of type: ignore

Ignore comments are how a strictly-typed codebase decays. A bare # type: ignore on an import line swallows the import-untyped it was written for and the genuine attr-defined that appears three refactors later. Nobody notices, because the line already had a comment on it.

The scope ladder — never go higher than necessary

  1. line-level coded ignore: # type: ignore[import-untyped]
  2. file-level # mypy: disable-error-code="..."
  3. per-module override in pyproject.toml
  4. global config

Two flags make the ladder enforceable. --enable-error-code ignore-without-code rejects a bare ignore. --warn-unused-ignores (already part of --strict) retires the graveyard: an ignore whose diagnostic no longer fires becomes an error itself.

Pyright diverges here in a way worth knowing. It uses # pyright: ignore[rule], and its reportUnnecessaryTypeIgnoreComment is none even in strict mode. Stale suppressions rot silently there; if pyright is your only checker, nothing expires them.

Your task

You are handed a module’s suppression state as data — because deciding what to do with an ignore requires knowing what mypy says with the ignore removed, which is a fact about a checker run, not about the source.

def solve(current: list[str], reported: list[list[str]]) -> dict[str, object]:
  • current[i] — the trailing comment on line i today ("" if none)
  • reported[i] — the error codes mypy emits on line i with all suppressions removed

Produce lines[i], the minimal correct replacement:

  • no reported codes"". The ignore is unnecessary; --warn-unused-ignores would flag it, so delete it.
  • every reported code is in SUPPRESSIBLE"# type: ignore[a, b]", codes de-duplicated, sorted, joined with ", ".
  • otherwise"# FIX: a, b" over all the reported codes, sorted. This line does not get suppressed; it gets fixed.

SUPPRESSIBLE is given: import-untyped, import-not-found, no-any-unimported, override. The principle behind the list is the one that matters — a suppression is defensible only when the cause is outside the module. A dependency ships no annotations; a third-party base class has a signature you cannot change. An attr-defined or an arg-type is your own code being wrong, and suppressing it converts a compile-time error into a production one.

Also return:

key value
"suppressions" how many lines end up with a # type: ignore[...]
"fixes" how many lines need a human
"removed" lines that had a comment and now have none
"bare" how many of current are bare ignores (no [...])

The number that matters

"bare" is the audit metric. Once every suppression is coded, grep -c 'type: ignore\[' | sort by error code tells you where the type debt actually is — and --warn-unused-ignores starts expiring the dead ones for you.

Loading visualization…