Programming with AI
ENRU
6.3

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.py
measured
mypydemo.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()}"
measured
Success: no issues found in 1 source file
measured
greet("oslo")  -> Hello from OSLO
greet("paris") -> City not found

Why 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 mistakeCaught by types
Forgot to check for Noneyes
Mixed up a string and a numberyes
Called a method that doesn't existyes
Passed the wrong argumentsyes
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 scale

Then 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 = true

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

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

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

Prompt
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 None check is found before running.

  • On an existing project, switch strictness on module by module.

  • Any and # type: ignore are the places where checking is switched off.

  • A signature in the prompt is the most precise way to state a task.