Block 1 · Exercise 2
Type Hinting
- Type hints describe what a function takes and returns, and Python ignores them at run time
- A static checker such as mypy reads the hints and reports a mismatch before the code runs
In this exercise you annotate a small script and then let a static type checker verify it.
Work in a notebook in your own workspace. A type checker reads files rather than cells, so write the script to a file with write_text, the way Exercise 1 did, and keep editing that cell.
Step 1Basic annotations
Write a script typing_lab.py containing a function called calculate_stats that takes a list of numbers (integers) as input and returns a dictionary containing the sum, the average and the maximum value.
from pathlib import Path
Path("typing_lab.py").write_text('''
# your annotated function, and a call that prints the result
''')Run the file to see that it works, before the checker ever looks at it :
exec(Path("typing_lab.py").read_text())Annotate the function arguments and the return type using standard Python types (int, float, dict). Do not import List or Dict from typing ; use the modern built-in collection types that are available since Python 3.9.
def calculate_stats(numbers: list[int]) -> dict[str, float]:
...Call the function with a list of integers and print the result.
Step 2Complex types and unions
Modify your function so that it can handle "messy" data. The input list may now also contain strings that represent numbers, for example "100".
Update your type hints to indicate that the input list contains a union of integers and strings. Use the modern int | str syntax (Python 3.10+) or Union[int, str].
Inside the function, add logic to convert the strings to integers. If a value cannot be converted, return None instead of the dictionary. Update the return type hint so that the function now returns an optional dictionary, written as dict[str, float] | None.
def calculate_stats(numbers: list[int | str]) -> dict[str, float] | None:
...Test the function with a clean list, with a list containing "100", and with a list containing a value such as "abc".
Step 3Static analysis
Install mypy into this session and run it against your script :
%pip install mypyThen run the checker over your file :
import subprocess
import sys
report = subprocess.run(
[sys.executable, "-m", "mypy", "typing_lab.py"], capture_output=True, text=True
)
print(report.stdout)A %pip install lasts as long as the session and vanishes with it. On a machine of your own the same thing is uv add --dev mypy followed by uv run mypy typing_lab.py, and with the classic tooling pip install mypy followed by mypy typing_lab.py. The checker is the same program in all three.
A command-line tool is written to end its process with sys.exit, and a separate process gives it one to end. The same pattern runs pytest, ruff or any other tool from a notebook. mypy also has a Python entry point, mypy.api.run, which catches that exit and returns the report as strings ; try it and compare.
Now introduce a type error on purpose. Append a string to a list declared as list[int], or do arithmetic on a variable annotated as str. Write the file again and rerun the checker.
Study the report. Note the file, the line number and the error code mypy gives. Did the script still run? It did : annotations change nothing about what Python does, and a static checker reads the code without running it. Fix the error so that mypy reports Success: no issues found.
If time permits
- Create a generic function called
get_first_itemthat accepts a list of any typeTand returns a single item of typeT. UseTypeVarfrom thetypingmodule so that the checker knows the return value ofget_first_item(["a", "b"])is astr.
from typing import TypeVar
T = TypeVar("T")
def get_first_item(items: list[T]) -> T:
...- Assign the result to a variable annotated with the wrong type, run mypy again and confirm that the mistake is reported.
- Replace a status string in your script with a
Literal["ok", "error"]hint or aStrEnum, and check that an invalid value is rejected by mypy.