Skip to content

← Orientation and the Gate step 3 of 13

Easy Framework

Reading a mypy error: codes, reveal_type, assert_type

Every mypy diagnostic ends in a bracketed error code:

solution.py:12:11: error: Argument 1 to "load" has incompatible type
    "str"; expected "Path"  [arg-type]

That [arg-type] is the whole point of this lesson. It is the difference between

value = load(path)  # type: ignore

which silences every present and future error on that line — including the None-return bug you introduce next quarter — and

value = load(path)  # type: ignore[arg-type]

which silences exactly the one you triaged. Under this course’s gate --warn-unused-ignores is on, so a scoped ignore that stops being needed becomes an error in its own right and gets deleted. A blanket ignore never will. You cannot scope what you cannot name.

Two more instruments, both of which cost you nothing at runtime:

  • reveal_type(x) — a mypy-only pseudo-function. Drop it anywhere, run mypy, and it prints note: Revealed type is "builtins.str". It is not a real builtin: leave it in and the module raises NameError when it actually runs.
  • assert_type(x, str) from typing — a checked assertion. mypy errors if the inferred type is not exactly str; at runtime it returns its first argument unchanged. This is how you pin an inference in a test so that a refactor which silently widens something to Any fails CI instead of passing quietly.

The problem

--strict is not a check. It is thirteen checks bundled behind one flag (the companion article lists them). So a team’s mypy invocation is a claim about which checks are on, and in practice half the flags people append to --strict are already inside it — noise that makes the config unreadable and hides the two or three flags that are genuinely doing work.

Two module constants are given to you:

  • STRICT_IMPLIES — the thirteen flags --strict turns on.
  • BEYOND_STRICT — recognised flags that --strict does not turn on.

Write:

def explain_strict(flags: Sequence[str]) -> tuple[frozenset[str], frozenset[str]]:

returning (redundant, additive):

  • redundant — flags in the input that --strict already implies. Only possible when --strict is itself in the input; without it nothing is implied, so nothing is redundant.
  • additive — flags in the input that genuinely add a check.
  • --strict itself belongs to neither set.
  • Any flag in neither constant raises UnknownFlagError(flag), carrying the offending flag on a .flag attribute.

Then the graded entrypoint, which is the boundary — it must not let that exception escape:

def solve(flags: Sequence[str]) -> dict[str, frozenset[str]]:

Return {"redundant": ..., "additive": ..., "unknown": ...}. On an unknown flag, redundant and additive are empty and unknown holds the single flag that tripped — the first offender in input order, so the report is stable regardless of set iteration order.

What the gate exercises

frozenset[str], not bare frozenset. Under --disallow-any-generics — one of the thirteen — a bare frozenset annotation means frozenset[Any] and is an error. Final on the constants stops a later edit rebinding them. And the except UnknownFlagError as exc: branch is where the declaration earns its keep: mypy knows exc.flag is a str only because you declared it in __init__. Attach it with a bare exc.flag = flag from the raise site instead and you get "UnknownFlagError" has no attribute "flag" [attr-defined] — which is the checker correctly telling you that attributes bolted on from outside are invisible to every other reader of the class.

Loading visualization…