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.
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.
Python still runs if the value is wrong. Check JSON with isinstance at the edge.
Hints are notes, not locksWrite 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 ->.
| Hint | Meaning |
|---|---|
names: list[str] | A list of strings |
used: dict[str, int] | Keys are strings, values are ints |
label: str | A string |
ok: bool | True or False |
row: dict | A dict (values unspecified) |
args: dict[str, object] | JSON-like mapping |
q or None | string or missing |
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.
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 NoneDo 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:
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.
| Check | Catches | Misses |
|---|---|---|
hint k: int | nothing at run time | "3", True |
isinstance(x, int) | strings, dicts | True (bool is an int) |
type(x) is int | bools too | still not “in range” |
"q" in args | missing key | wrong type of value |
isinstance(q, str) and q.strip() | missing, numbers, blank text | valid 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
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 qThe 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
isinstanceat 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 | Nonewithout a None check. - Hinting
dict[str, int]on raw JSON and then doing math on a string.
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