We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 20 of 26
Write Your Own Narrowing Check with TypeIs
You move an isinstance check into a helper so you can reuse it. The check
still works at runtime, but mypy stops narrowing:
def is_str(x: object) -> bool:
return isinstance(x, str)
def shout(value: str | int) -> str:
if is_str(value):
return value.upper()
return str(value)
Item "int" of "str | int" has no attribute "upper"
A plain bool tells mypy nothing about value. Give the helper a return
type of TypeIs[str] instead. A function whose return type is TypeIs or
TypeGuard is a narrowing function. It returns a bool at runtime, and
its return type tells mypy what a True result means.
TypeIs[R]narrows both branches. When it returnsTrue, the value is anR. When it returnsFalse, the value is not anR, so mypy removesRfrom the type. Forvalue: str | int, theelsebranch getsint.TypeGuard[R]narrows only theTruebranch. Theelsebranch keeps the original type,str | int.
mypy trusts these annotations. It does not check that the function body
matches the annotation. This exercise includes one wrong annotation on
purpose, so you can see what happens. TypeIs needs Python 3.13 or newer.
What to write
is_str(x: object) -> TypeIs[str]returnsisinstance(x, str). Change the starter'sTypeGuardtoTypeIs.all_ints(xs: list[object]) -> TypeGuard[list[int]]returnsTruewhen every element has exactly the typeint. Usetype(x) is int, soTrue,2.0and subclasses ofintdo not count. An empty list counts. It must beTypeGuard, becauseTypeIsneeds the narrowed type to be a subtype of the input type, andlistis invariant.is_non_empty(s: str | None) -> TypeIs[str]returnsTruewhensis notNoneand is not empty. A string of spaces is not empty. This annotation is deliberately wrong. Keep it, sounsoundcan show the problem.classify(items: list[object]) -> tuple[list[str], list[str]]usesis_str. Strings go into the first list asitem.upper(). For every other item,type(item).__name__goes into the second list. Keep the order and any duplicates. An empty string is a string.total(xs: list[object]) -> intreturnssum(xs)whenall_ints(xs)is true, and-1otherwise. An empty list gives0.unsound(values: list[str | None]) -> list[str]callsis_non_emptyon each value. OnTrue, it appendsf"set:{value}". Otherwise it appendsf"missing:{value!r}". Keep the input order.solve(items: list[object], numbers: list[object], maybe: list[str | None])returns(strings, others, total(numbers), unsound(maybe)), annotated astuple[list[str], list[str], int, list[str]]. Do not change the inputs.
Example
assert solve(["a", 1, None, "b"], [1, 2, 3], ["x", "", None]) == (
["A", "B"], ["int", "NoneType"], 6, ["set:x", "missing:''", "missing:None"],
)
Look at missing:''. In the else branch of unsound, mypy believes
value is None, because TypeIs[str] says that no string gets there.
The empty string gets there anyway. Nothing crashes here, but code that
relied on value being None would be wrong.
Going further
For a real "non-empty string" check, use TypeGuard[str], or return a plain
bool and check for None yourself. Use TypeIs[str] only when the check
accepts every string. On Python 3.12, the typing_extensions package
provides TypeIs. The typing specification
has the full rules.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.