When do you reach for typing.cast() versus a `# type: ignore` comment?
answer
- Two hatches, two different admissions
- One asserts a type, one mutes a report
- Runtime behaviour: nothing happens either way
- Prefer a real narrowing check first
- Name the error code in the brackets
basics
~20 sUse typing.cast when you know the value's real type and the checker cannot see it; it returns the value unchanged at runtime. Use a suppression comment, with its error code, only when the checker itself is wrong.
solid answer
~50 sThey are different admissions. `typing.cast(T, value)` says *the type here is T, trust me* — the checker adopts T from that point on, and at runtime `cast` simply returns its second argument, checking nothing. A `# type: ignore[...]` comment says *there is an error on this line and I am not fixing it now*; it silences the report, but the underlying inferred types are unchanged, so the error can resurface at the next use. My preference order is: narrow the type properly first (an `isinstance` check or an `assert` actually validates at runtime), then `cast` when narrowing is impossible — for example a value coming out of an untyped boundary that I know the shape of — and only then a suppression. Suppressions always carry the specific error code, so they hide one known problem rather than every future one on that line.
code
python · 10 linesfrom typing import cast
raw: object = {"accession": "A-1"}
# Checker-only: at runtime this is just `raw`.
record = cast(dict[str, str], raw)
print(record["accession"])
lying = cast(dict[str, str], "not a mapping")
print(type(lying).__name__) # str - cast validated nothinggo deeper
Remember the one-line facts: typing.cast returns its argument unchanged and checks nothing, and a # type: ignore comment only silences a checker message. Neither changes what the program does when it runs.
Explain the mechanical difference: a cast changes the type the checker carries forward, a suppression only mutes one report while the inferred types stay put. Say why the bracketed error code matters.
Demonstrate the ordering habit — narrow first, cast at boundaries, suppress last — and describe how you keep hatches auditable: a reason comment on each, a count you watch, and review attention equal to a swallowed exception.
Frame escape hatches as debt with a direction. Be ready to argue when a widening cast count means the migration is buying silence rather than safety, and what policy you would set for teams shipping suppressions.
## Two escape hatches, two different lies Gradual typing needs escape hatches, and a legacy migration uses them constantly. The discipline is knowing which one you are reaching for and leaving evidence of why. **`typing.cast(T, value)`** is a *checker-only* assertion. To the checker, the expression has type `T` from that point onward. At runtime it is a plain function that returns `value` untouched — no validation, no conversion, no error if the value is nothing like `T`. That is the whole implementation, and it is why `cast` is cheap and why it is dangerous: a wrong cast produces no symptom at all until something much later crashes with an `AttributeError` or a `KeyError` on a value everyone believed had been checked. **`# type: ignore[some-code]`** is a *report* suppression. It does not change any inferred type; it tells the checker not to print the error it found on that line. The types stay as they were, so the same mismatch will be reported again the next time it matters, at a different line. That difference matters when you are choosing: `cast` propagates a new belief forward, a suppression only mutes one complaint. ## What to try before either one Most of the time the honest fix is *narrowing*: an `isinstance` check, an `is not None` check, an `assert`, or restructuring so the type is obvious. Narrowing is strictly better than casting because it is real at runtime — if the value is not what you assumed, you get an immediate, local, informative failure instead of a silent lie that surfaces three modules away. In a legacy migration, an `assert isinstance(...)` at a boundary is often the right first move: it documents the assumption, it teaches the checker, and it fails fast the first time reality disagrees. Sometimes narrowing is impossible — you cannot `isinstance` a `TypedDict`, a generic alias, or a protocol at runtime without cost, and some values arrive from a boundary you do not control. That is `cast`'s legitimate home: convert once, at the entry point, with the cast sitting right next to the code that justifies it. ## Always write the error code A bare `# type: ignore` silences everything on that line, forever, including errors that appear later for entirely different reasons. Writing the code in brackets narrows the suppression to the one class of problem you actually inspected; a new, unrelated error on the same line still surfaces. This is the single habit that keeps a legacy codebase's suppressions from decaying into blanket blindfolds. (Which codes exist and how the checker is configured belongs to the checker's own documentation; the habit of naming one is what matters here.) ## Keeping the hatches visible Escape hatches are debt, and debt needs an accounting. Two practices work well: - **A comment on every suppression.** Not what it suppresses — the checker already says that — but *why it cannot be fixed yet*: an untyped dependency, a shape a checker cannot express, a refactor scheduled behind it. - **A count you watch.** The absolute number of casts and suppressions matters less than its direction. A migration where the count falls as modules are annotated is healthy; one where every new annotation is accompanied by a fresh suppression has substituted silence for typing. In review, both deserve the same attention as a swallowed exception: a reviewer should be able to read the line and see the justification without opening the checker. ## The failure mode to name in an interview The worst outcome is a cast that is simply wrong. On a museum-catalogue importer, one `cast` on a record pulled out of an untyped parser convinced every downstream module that a field was a date object; it was a string, and the nightly run failed five hours in, deep inside reporting, with a message that pointed nowhere near the cast. Nothing in the type system could have caught it, because the cast *is* the point at which checking was switched off. That is the argument for narrowing first, casting at boundaries only, and treating both hatches as things you justify rather than things you sprinkle.
- Why is a bare `# type: ignore` worse than one carrying an error code?A bare suppression hides every error on that line for as long as it exists, including errors introduced later by unrelated changes. The bracketed form hides exactly the class of error you inspected, so a genuinely new problem on the same line is still reported. It also documents what was wrong, which the bare form does not.
- Where in the code should a cast live, and why does its position matter?As close as possible to the boundary where the unknown value enters, next to the evidence that justifies it. A cast placed deep inside business logic separates the assertion from the reason for it, so a reviewer cannot judge it and a later change can invalidate it silently. One cast at the entry point, feeding a named shape, is auditable; casts scattered through call chains are not.
- How do you check what the checker actually believes a value's type is?Insert `typing.reveal_type(value)` at the point of doubt: the checker reports the inferred type there. Since 3.11 it also exists at runtime and prints the runtime type before returning the value unchanged, so the line is harmless if you forget to remove it, though it should not survive review. It is the fastest way to find where a precise type degraded.
saying these in an interview costs you the question
- Thinks cast() converts or validates the value at runtime
- Reaches for a suppression comment before trying to narrow
- Leaves bare `# type: ignore` comments with no error code
- Says a suppression fixes the underlying inferred type
- Treats casts as free and never reviews or counts them