We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Structural Typing and the Hard Parts step 20 of 24
Replacing cast() with a TypeIs predicate
A wrong cast is worse than no annotation. No annotation leaves an unknown;
a wrong cast converts an unknown into a confidently-wrong known, and
every inference downstream builds on the lie. The traceback then surfaces
four frames away from the mistake.
cast generates no runtime code at all. It is an assertion to the
checker and nothing else.
The escalation ladder, best first
-
Fix the types. Usually the source of the
objectcan be typed properly. -
Narrow with
isinstance. Free, sound, checked. -
Write a
TypeIspredicate so exactly one audited function carries the risk, and it is unit-testable like any other function. -
assert isinstance(...)— fails loudly at the right line, at the cost of a runtime check (and disappears underpython -O). -
cast, with a comment naming the invariant that makes it safe. -
# type: ignore[code]— last, and coded.
--warn-redundant-casts catches a pointless cast. Nothing catches a
wrong one. That asymmetry is the whole argument.
TypeIs vs TypeGuard
TypeIs[T] (3.13) narrows in both branches and requires T to be
consistent with the parameter type — which is what you want almost always.
TypeGuard[T] narrows only the positive branch and permits an unrelated
T; reach for it only when the predicate genuinely converts rather than
narrows.
Your task
The starter passes mypy --strict cleanly and crashes on the first
malformed payload. Rewrite it so that no cast remains, --strict is still
clean, and malformed input produces a value rather than an exception.
class UserRecord(TypedDict):
name: str
roles: list[str]
def is_user_record(value: object) -> TypeIs[UserRecord]
def solve(records: list[object]) -> list[str]
is_user_record must actually check: a dict, a name that is a str, a
roles that is a list, and every element of roles a str.
solve returns, for each record, f"{name.upper()}:{len(roles)}" if it
validates, and the literal "invalid" otherwise.
Extra keys are accepted — a TypedDict is an open type, and the wire
format you are reading almost certainly grows fields you do not care about.
(closed=True from PEP 728 is how you would say otherwise.)
What the tests are really checking
A record of 42, of None, of [], of {"name": 3, ...}, and of
{"name": "bo", "roles": ["x", 1]} all arrive. Under the starter, the first
one raises TypeError: 'int' object is not subscriptable and the run ends.
Under a TypeIs predicate every one of them yields "invalid" and
processing continues — which is the difference between a bad record and a
bad deployment.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.