Skip to content

← Precise Type Hints with Literals and Generics step 3 of 26

Medium Primitives

Turn an Unknown Value into an int

Your service reads settings from a file that anyone can edit. A setting that should be a whole number might arrive as 7, "7", 7.0, True or None. You write one function that accepts the safe forms and rejects the rest.

assert solve([7, "007", "-5", 2.0, True, " 42 ", None]) == [
    "ok:7", "ok:7", "ok:-5", "ok:2",
    "reject:bool", "reject:str", "reject:NoneType",
]

Any, object and Never

  • Any switches off checking for that value, and mypy accepts any operation on it. The starter's coerce_int(value: Any) -> Any returns its input without checking it, and mypy reports no error.
  • object also accepts any value, but mypy allows only what every object supports, such as str(value). To call value.is_integer(), first narrow with isinstance(value, float). So object is the right type for a value you have not checked yet.
  • Never is the type with no values. A function annotated -> Never never returns normally, for example because it always raises. The exercise "Make mypy Report an Unhandled Event with assert_never" uses it.

What to write

coerce_int(value: object) -> int accepts three kinds of value:

Input Rule
int Return it. Reject bool.
str Accept one or more ASCII digits, with at most one - in front.
float Accept only when value.is_integer() is true.
  • Raise TypeError for every other value. Any message will do.
  • Check bool before int. bool is a subclass of int, so isinstance(True, int) is true.
  • For a string, remove one leading - if there is one, and call the rest body. Accept the string only when body.isascii() and body.isdecimal() is true.
  • For a float, check is_integer() before you call int(), because int(7.5) drops the fraction and returns 7. is_integer() is false for fractions, infinity and NaN.
  • Replace the starter's Any annotations with object and int. Narrow with isinstance, and use no cast or # type: ignore.

solve(values: list[object]) -> list[str] returns one string per value, in order:

  • "ok:<n>" for an accepted value, with n in decimal and no leading zeros.
  • "reject:<name>" for a rejected value, where the name is type(value).__name__.

By default, Python's int() and str() refuse numbers longer than 4300 digits, and the tests include longer ones. So convert a validated string with the starter's _parse_decimal, and format each result in solve with _decimal_text.

Edge cases

  • coerce_int rejects " 42 ", "+5", "1_0", "0x10", "" and "-", though int() accepts the first three.
  • It also rejects "٣", "1" and "³". isdigit() alone accepts all three, and isdecimal() alone accepts the first two.
  • "007" gives "ok:7". "-000" and -0.0 give "ok:0".
  • An empty list gives [].

Going further