You have now submitted two exercises, and mypy checked both. This function passes mypy in strict mode, with every option the site adds, and it still crashes:
import json
def load_name(raw: str) -> str:
data = json.loads(raw)
name: str = data["user"]["name"]
return name
name = load_name('{"user": {"name": 42}}')
print(name.upper())
AttributeError: 'int' object has no attribute 'upper'
The annotation says load_name returns a str. Here it returns the
integer 42. The crash happens in the caller, one step away from the real
mistake.
mypy does not check Any values
json.loads returns Any, the type mypy gives a value it knows nothing
about. When you assign an Any value to name: str, mypy accepts the
assignment and treats name as a str. It cannot know what the JSON text
will hold when the program runs.
mypy reads your code without running it, and checks that the code agrees with its annotations. It never sees the data your program receives. It cannot tell whether your logic does what you meant.
Fix the bug by checking the data where it enters your program:
import json
def load_name(raw: str) -> str:
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError("expected a JSON object")
user = data.get("user")
if not isinstance(user, dict):
raise ValueError("expected a user object")
name = user.get("name")
if not isinstance(name, str):
raise ValueError("expected a string name")
return name
Bad input now fails at once, with a message that says what is wrong. After
each isinstance check, mypy knows the real type, so it can check the code
that follows.
Mistakes mypy cannot catch
These mistakes all pass a strict type check:
- Trusting data from outside your program. JSON, files, environment variables and
network responses arrive as
Anyor as plain strings. Check them where they arrive, as above. - A wrong
cast.cast(int, raw)tells mypy to treatrawas anint. When the program runs,castreturnsrawunchanged, socast(int, "42")is still the string"42". Use anisinstancecheck instead. - An ignore comment over a real bug.
# type: ignore[arg-type]hides the error on its line and leaves the bug in place. - A misspelled override. If a subclass defines
handelinstead ofhandle, mypy sees a new method, and callers still get the parent'shandle. Put@override, fromtyping, on each method that you mean to override. mypy then reports any of them that has no parent method of that name. - Wrong values of the right type. mypy cannot tell
total - 1fromtotal + 1. Tests catch these.
A bug where every type is right
This function has correct annotations, and mypy accepts it:
def add_tag(tag: str, tags: list[str] = []) -> list[str]:
tags.append(tag)
return tags
print(add_tag("first")) # ['first']
print(add_tag("second")) # ['first', 'second']
Python creates the default list once, when it defines the function. Every
call that leaves out tags adds to that same list. Use None as the
default, and make a new list inside the function:
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
A test that calls add_tag twice catches this bug. mypy cannot.
Check your understanding Your function passes mypy and its tests, then crashes in production on a payload with a missing field. What should you add? Show answer Hide answer
Add a check where the payload enters your program. mypy never sees the payload, and your tests cover only the payloads you thought of. Check each field you use, raise an error that names the missing field, and add the failing payload to your tests.
Each check catches different bugs
mypy finds code that uses a value in a way its type does not allow, including in branches your tests never run. Tests check what the code does on the inputs you chose. Checks where data enters your program reject bad data. Use all three, because each one catches bugs that the other two miss.
Going further
--disallow-any-explicitrejectsAnywritten in your own annotations. The site does not turn it on, but some projects do.--enable-error-code explicit-overridemakes mypy require@overrideon every method that overrides a parent method.- Ruff's B006 rule reports mutable default arguments.
- Libraries such as Pydantic check JSON against a class you define, in
place of a chain of
isinstancechecks.
Read more in mypy's optional error codes
and the typing module documentation.