Typing, seriously
making annotations check the AI's code
What it is
Article 2.4 explained how to read type annotations. Here is how to turn them from a comment into a check.
On their own, Python's annotations check nothing: write -> str, return
None, and it stays silent. But there is a tool that reads and checks them,
and for working with an AI this is one of the highest-return techniques in
the whole book.
What it looks like in code
Code that looks right:
def find_city(name: str) -> str | None:
cities = {"oslo": "Oslo", "bergen": "Bergen"}
return cities.get(name.lower())
def greet(name: str) -> str:
city = find_city(name)
return f"Hello from {city.upper()}"It runs without errors — as long as the city requested exists. The check:
python3 -m mypy --strict mypydemo.pymypydemo.py:8: error: Item "None" of "str | None" has no attribute "upper" [union-attr]
Found 1 error in 1 file (checked 1 source file)Which translates as: find_city can return None, and None has no
upper method. The error was found before running, with no tests, no
reproduction, and no users.
After the fix:
def greet(name: str) -> str:
city = find_city(name)
if city is None:
return "City not found"
return f"Hello from {city.upper()}"Success: no issues found in 1 source filegreet("oslo") -> Hello from OSLO
greet("paris") -> City not foundWhy this matters specifically when working with an AI
Go back to chapter 4: a whole chapter on catching by hand the errors you cannot see at runtime. Type checking catches a whole class of them mechanically:
| AI's mistake | Caught by types |
|---|---|
Forgot to check for None | yes |
| Mixed up a string and a number | yes |
| Called a method that doesn't exist | yes |
| Passed the wrong arguments | yes |
| Invented a library method (article 4.8) | yes, if the library ships types |
It does not replace reading the code. But of the four mandatory checks (article 4.1), one is now done by a machine, every time, for free.
How to start on an existing project
Checking strictly from the outset means drowning in hundreds of errors. A sensible path:
pip install mypy
python3 -m mypy weatherbot/ # without --strict, to see the scaleThen module by module: add annotations, get it clean, move to the next. The
settings go in pyproject.toml:
[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "weatherbot.storage.*"
strict = trueThat way strictness is switched on where order has already been restored.
What to tell the AI
Types have to be asked for explicitly — by default they are not written everywhere:
Add type annotations to every function in this module.
Make sure mypy --strict passes. Don't change the logic.A strong technique is asking it to check itself:
Run mypy --strict over your own code in your head: which
errors would it find? If it would find any, fix them now.And the most useful one — types as a way of stating the task:
The signature must be:
def parse_report(raw: str) -> list[CityTemp]:
where CityTemp is a dataclass with fields name: str and temp: float | None.
Write the implementation to fit that signature.That makes types part of stating the task (article 3.4): they are more precise than words and they are checked by a machine.
What to look for
Annotations present, mypy never run. The most common case: the types
were written for looks and drifted away from the code long ago.
Any everywhere. def handle(data: Any) -> Any is an annotation that
promises nothing. The AI puts Any where it isn't sure; that spot is worth
pinning down.
# type: ignore with no explanation. A way of silencing the checker.
Sometimes justified, but the comment should say why.
-> None on a function that returns something. Annotations are not
checked at runtime, and a mismatch like that survives for years.
Types only in signatures, without --strict. Without strict mode
mypy skips untyped functions entirely — the check turns out to be an
appearance.
Cheat sheet
Annotations check nothing by themselves; you need
mypy.python3 -m mypy --strict file.py— one command, catches a whole class of AI errors.The most valuable part: a forgotten
Nonecheck is found before running.On an existing project, switch strictness on module by module.
Anyand# type: ignoreare the places where checking is switched off.A signature in the prompt is the most precise way to state a task.