Write a fully annotated function using built-in generics and a union type, and show what the annotations look like at runtime.
Type hints are notes that you attach to the parameters and the return value of a function. Since Python 3.9 you can use the built-in container types directly as subscripts, and since Python 3.10 you can write a union with the pipe operator, so most of the typing module is no longer needed for everyday code.
def summarise(items: list[int], *, scale: float = 1.0) -> tuple[int, float]:
total = sum(items)
return total, total * scale
print(summarise([1, 2, 3])) # (6, 6.0)
print(summarise.__annotations__)
The annotations dictionary reads items: list[int], scale: float and return: tuple[int, float]. Notice that the keyword-only marker is a plain asterisk with no annotation, and that is completely normal.
Two details are worth pointing out. The annotations are stored on the function object, so they are available while the program is running, and you can read them directly or resolve any string annotations with typing.get_type_hints. And because the annotations are evaluated when the def statement runs, an annotation that refers to a name which does not exist yet raises a NameError at definition time. That is the reason why from __future__ import annotations exists, because it postpones the evaluation and turns every annotation into a plain string.
Here is the rest of the syntax you should be able to recognise. Optional[int] is now written as int | None. A fixed sequence of one type is Sequence[str]. A callable is Callable[[int, str], bool]. And a class that can be either of two unrelated types is written as a union. If you see old code that uses List and Dict from the typing module, then it was written before Python 3.9.