Warm-up · Activity 1 of 7
// A2.1 · ~30 min · Advanced
Type hints and a type checker
After this lesson you can annotate functions and variables with list[int], dict[str, float] and X | None, run mypy and read what it reports, and explain why Python itself never checks a hint.
Lesson 1 of 6 in A2 Typing
You will be able to
- Annotate parameters, return values and variables with built-in generics and X | None
- Run mypy on a file and read its messages and error codes
- Predict what hints do at runtime: they are stored, never checked
Predict · Activity 2 of 7
Predict before you read on: n is annotated as int. What does this print?
def double(n: int) -> int: return n * 2 print(double("ab"))Practice · Activity 3 of 7
first() returns the first item, or None for an empty list. Fill in the operator so the return hint says "an int or None".
def first(items: list[int]) -> int ____ None: return items[0] if items else Noneint None:Practice · Activity 4 of 7
mypy reports two errors on line 4 of this file. Which error code does it put in brackets at the end of both?
def total(prices: list[float]) -> float: return sum(prices) print(total(["3.50", "1.20"]))Practice · Activity 5 of 7
Match each annotation to a value it describes.
Brain teaser · Activity 6 of 7
Brain teaser. You run python -m mypy main.py, with no options, on this file. What is the last line mypy prints?
def load(): count: int = "three" return countApply · Activity 7 of 7
Mini-task, on your own Python with mypy installed. Write parse_price(text), which returns the float in text or None when text is not a number, and total(prices), which adds a list of such results and skips the None values. Annotate both, then run python -m mypy --strict on the file until it reports no issues.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Hints stored, not checked
Two annotated functions and an annotated variable. The program prints the stored hints, then calls mean() with a tuple of ints, which its hint does not allow. Python runs the call anyway. Without the # type: ignore[arg-type] comment, mypy reports that line: error: Argument 1 to "mean" has incompatible type "tuple[int, int, int]"; expected "list[float]" [arg-type].
main.py
# Hints describe types; Python stores them but never checks them.
def mean(values: list[float]) -> float:
return sum(values) / len(values)
def find(stock: dict[str, int], name: str) -> int | None:
return stock.get(name)
stock: dict[str, int] = {"apples": 4, "pears": 0}
print(mean.__annotations__)
print(find.__annotations__["return"])
print(mean([1.5, 2.5]))
# A tuple of ints: mypy objects, and this comment silences that error.
print(mean((1, 2, 3))) # type: ignore[arg-type]
print(find(stock, "plums"))
Run it with
python main.pyOutput
{'values': list[float], 'return': <class 'float'>}
int | None
2.0
2.0
None- __annotations__ holds the hints as objects: list[float] is a generic alias, float is the class itself.
- int | None is a union object; it prints the way you wrote it.
- mean((1, 2, 3)) works, because sum() and len() accept a tuple. Only mypy objects, and the comment silences exactly that error code.
- find() returns None for a missing name, which is what the | None in its hint promises.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 2
Annotate the price helpers
The three functions work, but have no hints. Annotate them: parse_price takes a str and returns a float or None; cheapest takes a list of floats and returns a float or None; label takes a name (str) and a price that may be None, and returns a str. The tests read the hints with typing.get_type_hints, so write them exactly as described.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
The pattern is def name(param: Type) -> ReturnType:. A result that may be missing is float | None.
Hint 2
A list of floats is list[float]; no import is needed for list or for the | operator.
Hint 3
label(name: str, price: float | None) -> str: the price parameter may be None, so its hint says so.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
def parse_price(text: str) -> float | None:
try:
return float(text)
except ValueError:
return None
def cheapest(prices: list[float]) -> float | None:
if not prices:
return None
return min(prices)
def label(name: str, price: float | None) -> str:
if price is None:
return f"{name}: no price"
return f"{name}: {price:.2f} EUR"
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
def parse_price(text):
try:
return float(text)
except ValueError:
return None
def cheapest(prices):
if not prices:
return None
return min(prices)
def label(name, price):
if price is None:
return f"{name}: no price"
return f"{name}: {price:.2f} EUR"
test_main.py
from typing import get_type_hints
from main import cheapest, label, parse_price
def test_parse_price_hints():
"""parse_price takes a str and returns float | None"""
got = get_type_hints(parse_price)
assert got == {"text": str, "return": float | None}, f"the hints of parse_price are {got!r}"
def test_cheapest_hints():
"""cheapest takes list[float] and returns float | None"""
got = get_type_hints(cheapest)
assert got == {"prices": list[float], "return": float | None}, f"the hints of cheapest are {got!r}"
def test_label_hints():
"""label takes a str and a float | None, and returns a str"""
got = get_type_hints(label)
assert got == {"name": str, "price": float | None, "return": str}, f"the hints of label are {got!r}"
def test_behaviour():
"""The functions still work: 2.5, None, 1.25 and the two labels"""
got = parse_price("2.5"), parse_price("free"), cheapest([2.5, 1.25]), cheapest([])
assert got == (2.5, None, 1.25, None), f"the functions returned {got!r}"
got_labels = label("tea", 2.5), label("cake", None)
assert got_labels == ("tea: 2.50 EUR", "cake: no price"), f"label returned {got_labels!r}"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
Make mypy clean
Run this one on your own Python: install mypy (python -m pip install mypy), then run python -m mypy --check-untyped-defs main.py. It reports two errors, and the first is a real bug: read_scores keeps the points as strings. Fix both so mypy reports no issues and the tests pass. Change the code where the hint is right, and the hint where the code is.
This exercise needs Python on your computer (the browser version cannot run it). The files and commands are below.
Hints
Hint 1
mypy infers scores = {} from the first value you store in it, so it sees dict[str, str] and says the return value does not match.
Hint 2
The hint dict[str, int] is right; convert the points with int(points).
Hint 3
Dividing with / always gives a float, so the return hint of average is the part to change: -> float.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
def read_scores(lines: list[str]) -> dict[str, int]:
scores: dict[str, int] = {}
for line in lines:
name, points = line.split(",")
scores[name] = int(points)
return scores
def average(scores: dict[str, int]) -> float:
return sum(scores.values()) / len(scores)
def best(scores: dict[str, int]) -> str | None:
if not scores:
return None
return max(scores, key=lambda name: scores[name])
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
def read_scores(lines: list[str]) -> dict[str, int]:
scores = {}
for line in lines:
name, points = line.split(",")
scores[name] = points
return scores
def average(scores: dict[str, int]) -> int:
return sum(scores.values()) / len(scores)
def best(scores: dict[str, int]) -> str | None:
if not scores:
return None
return max(scores, key=lambda name: scores[name])
test_main.py
from main import average, best, read_scores
def test_mypy_is_clean():
"""mypy --check-untyped-defs reports no errors for main.py"""
from mypy import api
report, errors, status = api.run(["--check-untyped-defs", "--no-error-summary", "main.py"])
assert status == 0, f"mypy found problems:\n{report}{errors}"
def test_read_scores():
"""read_scores turns "ada,3" into {"ada": 3}"""
got = read_scores(["ada,3", "bob,5"])
assert got == {"ada": 3, "bob": 5}, f"read_scores returned {got!r}, expected ints as values"
def test_average():
"""The average of 3 and 5 is 4.0"""
got = average({"ada": 3, "bob": 5})
assert got == 4.0, f"average returned {got!r}, expected 4.0"
def test_best():
"""best names the highest score, and returns None for no scores"""
got = best({"ada": 3, "bob": 5}), best({})
assert got == ("bob", None), f"best returned {got!r}, expected ('bob', None)"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
Expecting the hint to check the argument
def mean(values: list[float]) -> float:
return sum(values) / len(values)
print(mean(["1.5", "2.5"]))
What Python prints
TypeError: unsupported operand type(s) for +: 'int' and 'str'Why, and the fix
The hint list[float] did not stop the strings: Python ran the call, and sum() failed inside the function, far from the real mistake. Hints are checked only by a type checker. Run python -m mypy main.py, which reports List item 0 has incompatible type "str"; expected "float" on the call line, and convert the input where it enters the program.
Using an X | None result as if it were an X
def find(names: dict[str, str], key: str) -> str | None:
return names.get(key)
print(find({"ada": "Ada Lovelace"}, "bob").upper())
What Python prints
AttributeError: 'NoneType' object has no attribute 'upper'Why, and the fix
str | None promises that the result may be None, so the caller must deal with that before calling a str method. mypy reports it without running anything: Item "None" of "str | None" has no attribute "upper" [union-attr]. Test first, if name is not None:, or give a default with names.get(key, "").
Thinking a variable hint converts the value
count: int = "3"
print(count + 1)
What Python prints
TypeError: can only concatenate str (not "int") to strWhy, and the fix
An annotation on a variable converts nothing: count still holds the string "3". Convert explicitly, count: int = int("3"). mypy flags the original line: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment].
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.