SpiralTrain
Exercises › Block 1 · Exercise 2

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.

python
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 :

python
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.

python
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.

python
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 :

python
%pip install mypy

Then run the checker over your file :

python
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_item that accepts a list of any type T and returns a single item of type T. Use TypeVar from the typing module so that the checker knows the return value of get_first_item(["a", "b"]) is a str.
python
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 a StrEnum, and check that an invalid value is rejected by mypy.

Tried it yourself first?

The solution is a spoiler. Work through the hints first : a wrong attempt teaches more than a solution you only read.