JJoeven

Curriculum/Python

Type Hints

Write list[str], dict[str, int], and X | None. Hints help you and your editor. They do not stop bad JSON at runtime.

intermediate20 min27 / 37

A type hint is a note on a name. It says what type you expect. Python does not enforce that note by itself. The program will still run if a caller passes the wrong thing. That surprise is the lesson.

Joeven runs Python 3.12 (Pyodide). So you can write list[str] and str | None. The X | None form needs Python 3.10 or newer. Older code used List[str] from typing. New code uses built-in generics: list[str], dict[str, int], tuple[str, int].

Hints help you and your editor. They do not stop a model from sending bad JSON. Checks do.

Hints are notes, not locks
list of stra dictstr or None

Python still runs if the value is wrong. Check JSON with isinstance at the edge.

Hints are notes, not locks

Write hints on public functions first: tools, parsers, goal_satisfied. Skip hints on a two-line loop inside a tryit if they add noise. When a name is | None, the next line should handle None. If it does not, the hint is a lie.

list[str] and dict[str, int]

Write the type after a colon. Write the return type after ->.

HintMeaning
names: list[str]A list of strings
used: dict[str, int]Keys are strings, values are ints
label: strA string
ok: boolTrue or False
row: dictA dict (values unspecified)
args: dict[str, object]JSON-like mapping
q or Nonestring or missing
python
def tool_names(trace: list[dict]) -> list[str]:
    return [row[class="tok-s">"name"] for row in trace]

The hint says “I mean to return a list of strings.” You can still return something else. Python will still run. A checker like mypy on your laptop can warn. This browser will not.

list[dict] does not specify the dict’s keys. You can write dict[str, object] when values mix. You cannot express “must have key q” in a hint alone. That is isinstance and "q" in args.

A return type of list[str] does not freeze the list’s contents. Callers can still append. Hints describe intent, not a lock. tuple[str, ...] means a tuple of strings of unknown length. You do not need that for this track.

None with X | None

None means “missing.” Write str | None when a value can be text or missing.

Older code used Optional[str] from typing. That means the same thing as str | None. Prefer str | None in new code.

python
def last_error(trace: list[dict]) -> str | None:
    for row in reversed(trace):
        err = row.get(class="tok-s">"error")
        if isinstance(err, str):
            return err
    return None

Do not mark every value as | None “just in case.” Then every caller must check None. Mark it when missing is part of the contract: no error yet, no query yet, tool skipped.

reversed(trace) walks from the last step. That is the usual “latest error” search. The return type documents that you might find nothing.

When the return is str | None, the caller writes if err is None: before using err as text. If the caller concatenates without a check, you get TypeError. The hint warned a human. Runtime still needs the if.

Hints do not run

Hints are notes. They are not checks. This still runs:

python
def greet(name: str) -> str:
    return name

print(greet(12))

12 is not a string. Python does not stop it. A tool like mypy can warn on your machine. The running program does not care unless you check.

You can even assign against a hint: raw: dict[str, int] = {"k": "3"}. The hint says int. The value is str. Runtime is silent. That is why JSON is dangerous: it looks like your types and is not.

Check with isinstance

At the trust edge — data from JSON, a model, or a file — check the real type.

isinstance(value, str) is True if value is a string. Check before you use the value as text. isinstance(x, int) is True for ints. bool is a subclass of int in Python, so isinstance(True, int) is True. If you must reject booleans, check type(x) is int or check bool first. For token counts, also reject True.

isinstance(x, list) does not check the items inside. Loop and check each item if you need that.

isinstance(x, dict) does not check keys. Loop items if you need string keys.

CheckCatchesMisses
hint k: intnothing at run time"3", True
isinstance(x, int)strings, dictsTrue (bool is an int)
type(x) is intbools toostill not “in range”
"q" in argsmissing keywrong type of value
isinstance(q, str) and q.strip()missing, numbers, blank textvalid text that is the wrong query

Treat hints as a map for humans. Treat isinstance as a gate for machines.

Walkthrough: parse, then check, then call

python
def parse_query(args: dict[str, object]) -> str | None:
    q = args.get(class="tok-s">"q")
    if not isinstance(q, str):
        return None
    if not q.strip():
        return None
    return q

The hint on args did not save you. The isinstance gate did. A number 12 returns None. Missing q returns None. Spaces-only returns None. The tool search(q: str) can assume a real string because you already checked. Put the hint on search. Put the gate in parse_query. Tests should send a bad q and expect None or an error dict, not a TypeError from inside search.

Bad JSON

A model might send "k": "3" when you wanted an int. A hint dict[str, int] will not catch that. isinstance will. Convert with int only after you know it is a string of digits, or catch ValueError.

Do not send type hints to the model and hope. The model gets a docstring or a JSON schema (later tracks). Python hints are for people and checkers. Runtime validation is for data you did not create.

What goes wrong

  • Believing hints enforce types at run time.
  • Marking everything | None.
  • Skipping isinstance at the JSON edge.
  • Using old List[str] in new 3.12 code without need (it still works; list[str] is shorter).
  • isinstance(True, int) surprises in flags vs counts.
  • Concatenating a str | None without a None check.
  • Hinting dict[str, int] on raw JSON and then doing math on a string.
Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

parse_query returns None for a number. The hint on args did not save you. The isinstance gate did. The last prints show why token counts should reject True.

How agents use this

Tool arguments arrive as JSON. JSON does not know your Python hints. Write list[str] so you remember the contract. Then check with isinstance before you call the tool. str | None is right for “no error yet.” Hints help the team. Checks keep a bad model reply from crashing the loop.

Put hints on your functions: def call_tool(name: str, args: dict[str, object]) -> dict. Put checks inside: name is str, args is dict, then each field. Tests should send a bad q and expect a dict error, not a TypeError from inside search.

A good boundary is three layers. Hints on the inner tool (q: str). A parser that returns str | None or an error dict. An executor that never calls the inner tool on None. If you hint the inner tool as q: str | None “to be safe,” you pushed the mess inward. Keep the core function strict. Validate at the edge.

Checkers (mypy) are optional on this site and useful on a laptop. They catch your mistakes: returning None from a function hinted as str. They do not catch the model. Runtime isinstance catches the model. You need both stories: one for code you wrote, one for data you did not.

Check your understanding

Do type hints stop a wrong JSON value when the program runs?