We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 3 of 24
Coerce to int at an untyped boundary
Write the boundary function that refuses to launder Any.
Here is how a “fully typed” codebase gets zero real type coverage, invisibly:
data = json.loads(raw) # data: Any
x: str = data # type-checks. no error. no warning.
reveal_type(x) # Revealed type is "builtins.str"
mypy now believes, with total confidence and zero justification, that x is a
str. Every downstream annotation derived from it is fiction. Any is not
“unknown” — it is “assume whatever the surrounding code needs”, in both
directions. That is why one un-narrowed ORM row, one json.loads, one
**kwargs: Any, propagates untypedness through an entire call graph while the
CI badge stays green.
The three-way distinction you need:
-
Anyis compatible with everything in both directions. It disables checking.def f(x: Any) -> Any: return x.whatever()passes--strictcleanly, which is one of the eight things--strictdoes not catch. -
objectis the top of the type hierarchy: everything is assignable to it, nothing is assignable from it. Touch any attribute and you get anattr-definederror until you narrow. It isAny‘s honest twin, and it is almost always what you actually meant. -
Never(NoReturn) is the bottom: no value has this type. It is what a function that always raises returns, and whatassert_nevertakes.
object is the correct annotation for a value arriving from outside your
program, because it forces you to write the narrowing you were going to skip.
The task
def coerce_int(value: object) -> int:
def solve(values: list[object]) -> list[str]:
coerce_int accepts exactly these and returns the integer:
-
an
int; -
a
strmade of ASCII decimal digits, with an optional leading-; -
a
floatthat is integral (is_integer()is true).
Everything else raises TypeError. solve maps a list through it, producing
"ok:<n>" for accepted values and "reject:<typename>" for rejected ones,
where the type name is type(value).__name__.
The three traps, all of which the tests exercise
bool is an int subclass. isinstance(True, int) is True, and
True + 1 == 2. Silently coercing a boolean to 1 is how a feature flag
becomes a quantity. Reject it — and reject it first, because any
isinstance(value, int) check written before the bool check will swallow it.
int() is far more permissive than you think. int(" 42 ") strips
whitespace. int("1_0") is 10, because PEP 515 underscores are accepted in
numeric literals and in int(). Neither is a decimal-digit string, and
neither should pass a boundary that is supposed to be strict.
str.isdigit() is not “is an ASCII digit”. "٣".isdigit() is True and
int("٣") returns 3 (Arabic-Indic digits are decimal). "³".isdigit() is
also True, but int("³") raises ValueError, because a superscript is a
digit that is not a decimal digit. "1" (fullwidth) is decimal too. If your
validator and your parser disagree about what a digit is, you have a
ValueError escaping from code you believed was total. Require
isascii() and isdecimal() and the disagreement disappears.
Types
object is doing the work here: every branch has to narrow before it can touch
the value, which is exactly the discipline you want at a boundary. This problem
is graded with --disallow-any-explicit on top of --strict — the word Any
must not appear in your submission, and neither may cast. If you reach for
either, the narrowing you skipped is the bug you were asked to prevent.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.