Warm-up · Activity 1 of 7
Warm-up from lesson A2.1: the hint says int, but what does this print?
def half(x: int) -> int:
return x / 2
print(half(3))// A2.6 · ~45 min · Advanced
After this lesson you know when Python 3.14 evaluates annotations, you can read them safely with annotationlib, and you have typed a whole module step by step until mypy --strict reports no issues.
Lesson 6 of 6 in A2 Typing
You will be able to
Warm-up · Activity 1 of 7
def half(x: int) -> int:
return x / 2
print(half(3))Predict · Activity 2 of 7
def func(a: Cls) -> None:
print(a)
class Cls:
pass
print(func.__annotations__)Practice · Activity 3 of 7
get_annotations(f, format=Format.____)Practice · Activity 4 of 7
def expensive():
print("evaluating", end=" ")
return int
def f(x: expensive()) -> None:
pass
print("defined", end=" ")
f.__annotations__
f.__annotations__
print("done")Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
from __future__ import annotations
from annotationlib import get_annotations
def scale(n: int) -> int:
return n
print(get_annotations(scale)["n"] is int, get_annotations(scale, eval_str=True)["n"] is int)Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Node refers to itself without quotes, which works because annotations are evaluated lazily. price uses Decimal, which is imported only for the type checker. Reading its annotations in the STRING and FORWARDREF formats works; the default VALUE format raises NameError. The program passes mypy --strict.
main.py
from annotationlib import Format, get_annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from decimal import Decimal
class Node:
def __init__(self, value: int, next: Node | None = None) -> None:
self.value = value
self.next = next
def price(amount: Decimal) -> str:
return f"{amount:.2f}"
print(get_annotations(Node.__init__))
print(get_annotations(price, format=Format.STRING))
ref = get_annotations(price, format=Format.FORWARDREF)["amount"]
print(type(ref).__name__, ref.__forward_arg__)
try:
get_annotations(price)
except NameError as error:
print("VALUE:", error)
Run it with
python main.pyOutput
{'value': <class 'int'>, 'next': __main__.Node | None, 'return': None}
{'amount': 'Decimal', 'return': 'str'}
ForwardRef Decimal
VALUE: name 'Decimal' is not definedTab 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.
Exercise 1 of 4
This module reads shop items from lines like "pen; 1.50" and has no hints. Annotate every parameter and return value: name is a str, price a float, tags a list[str] or None, and cheapest returns an Item or None. total returns a float. Do not change the code itself.
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.
Methods need -> None when they return nothing: def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
Item is defined above the functions, and even if it were not, 3.14 would evaluate the hints lazily.
cheapest returns None for an empty list, so its return hint is Item | None.
One way to solve it. Yours can look different and still pass the checks.
class Item:
def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line: str) -> Item:
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text: str) -> list[Item]:
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items: list[Item]) -> Item | None:
if not items:
return None
return min(items, key=lambda item: item.price)
def total(items: list[Item]) -> float:
return sum(item.price for item in items)
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
class Item:
def __init__(self, name, price, tags=None):
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line):
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text):
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items):
if not items:
return None
return min(items, key=lambda item: item.price)
def total(items):
return sum(item.price for item in items)
test_main.py
from annotationlib import get_annotations
import main
PARAMETERS = {"parse_item": ["line"], "load": ["text"], "cheapest": ["items"], "total": ["items"]}
def test_every_function_is_annotated():
"""Every parameter and every return value has an annotation"""
for name, params in PARAMETERS.items():
hints = get_annotations(getattr(main, name))
missing = [p for p in [*params, "return"] if p not in hints]
assert not missing, f"{name} has no annotation for {missing}"
hints = get_annotations(main.Item.__init__)
missing = [p for p in ["name", "price", "tags", "return"] if p not in hints]
assert not missing, f"Item.__init__ has no annotation for {missing}"
def test_precise_types():
"""cheapest may return None, total returns a float, and tags may be None"""
assert get_annotations(main.cheapest)["return"] == (main.Item | None), "cheapest should return Item | None"
assert get_annotations(main.total)["return"] is float, "total should return float"
assert get_annotations(main.Item.__init__)["tags"] == (list[str] | None), "tags should be list[str] | None"
def test_behaviour():
"""load, cheapest and total still work"""
items = main.load("pen; 1.50\nbook; 12\n")
got = main.cheapest(items).name, main.total(items), main.cheapest([])
assert got == ("pen", 13.5, None), f"cheapest and total gave {got!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 4
The module now has hints, and a new function summary. mypy reports it: Item "None" of "Item | None" has no attribute "name" [union-attr]. The bug is real: summary([]) crashes. Narrow the result of cheapest so that an empty list gives "no items", and keep the text for a non-empty list.
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.
cheapest returns Item | None, and None has no attribute name.
Store the result in best, then test if best is None: return "no items".
After that test, mypy narrows best to Item, and the last line type-checks.
One way to solve it. Yours can look different and still pass the checks.
class Item:
def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line: str) -> Item:
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text: str) -> list[Item]:
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items: list[Item]) -> Item | None:
if not items:
return None
return min(items, key=lambda item: item.price)
def total(items: list[Item]) -> float:
return sum(item.price for item in items)
def summary(items: list[Item]) -> str:
best = cheapest(items)
if best is None:
return "no items"
return f"{len(items)} items, {total(items):.2f} in total, cheapest: {best.name}"
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
class Item:
def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line: str) -> Item:
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text: str) -> list[Item]:
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items: list[Item]) -> Item | None:
if not items:
return None
return min(items, key=lambda item: item.price)
def total(items: list[Item]) -> float:
return sum(item.price for item in items)
def summary(items: list[Item]) -> str:
best = cheapest(items)
return f"{len(items)} items, {total(items):.2f} in total, cheapest: {best.name}"
test_main.py
from main import load, summary
def test_summary():
"""Two items give a count, the total and the cheapest name"""
got = summary(load("pen; 1.50\nbook; 12\n"))
assert got == "2 items, 13.50 in total, cheapest: pen", f"summary returned {got!r}"
def test_empty():
"""An empty list gives "no items\""""
got = summary([])
assert got == "no items", f"summary([]) returned {got!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 3 of 4
Write signature_table(functions): for each function, one line such as parse_item(line: str) -> Item, built from annotationlib.get_annotations with format=Format.STRING. Every function passed has a return annotation. It must also work for functions whose annotations name classes that do not exist at runtime.
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 = annotationlib.get_annotations(function, format=annotationlib.Format.STRING) gives a dict of strings.
hints.pop("return") removes the return annotation, so the rest are the parameters, in order.
Join the parameters with ", ".join(f"{name}: {hint}" for name, hint in hints.items()) and use function.__name__ for the name.
One way to solve it. Yours can look different and still pass the checks.
import annotationlib
from collections.abc import Callable
class Item:
def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line: str) -> Item:
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text: str) -> list[Item]:
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items: list[Item]) -> Item | None:
if not items:
return None
return min(items, key=lambda item: item.price)
def total(items: list[Item]) -> float:
return sum(item.price for item in items)
def summary(items: list[Item]) -> str:
best = cheapest(items)
if best is None:
return "no items"
return f"{len(items)} items, {total(items):.2f} in total, cheapest: {best.name}"
def signature_table(functions: list[Callable[..., object]]) -> list[str]:
rows: list[str] = []
for function in functions:
hints = annotationlib.get_annotations(function, format=annotationlib.Format.STRING)
returns = hints.pop("return")
params = ", ".join(f"{name}: {hint}" for name, hint in hints.items())
rows.append(f"{function.__name__}({params}) -> {returns}")
return rows
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
import annotationlib
from collections.abc import Callable
class Item:
def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line: str) -> Item:
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text: str) -> list[Item]:
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items: list[Item]) -> Item | None:
if not items:
return None
return min(items, key=lambda item: item.price)
def total(items: list[Item]) -> float:
return sum(item.price for item in items)
def summary(items: list[Item]) -> str:
best = cheapest(items)
if best is None:
return "no items"
return f"{len(items)} items, {total(items):.2f} in total, cheapest: {best.name}"
def signature_table(functions: list[Callable[..., object]]) -> list[str]:
return []
test_main.py
from main import cheapest, parse_item, signature_table, summary
def test_table():
"""One line per function, with parameter and return annotations"""
got = signature_table([parse_item, cheapest, summary])
expected = ["parse_item(line: str) -> Item", "cheapest(items: list[Item]) -> Item | None", "summary(items: list[Item]) -> str"]
assert got == expected, f"signature_table returned {got!r}"
def test_undefined_names():
"""Undefined names in annotations do not raise"""
def later(x: Missing) -> Missing2:
pass
got = signature_table([later])
assert got == ["later(x: Missing) -> Missing2"], f"signature_table returned {got!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 4 of 4
Run this one on your own Python: python -m pip install mypy, then python -m mypy --strict main.py. It reports four errors: an unused # type: ignore, a bare list, and the new untyped helper label, which labels calls. Fix them without changing what the code does, until mypy --strict reports no issues.
This exercise needs Python on your computer (the browser version cannot run it). The files and commands are below.
Unused "type: ignore" comment: the ignore on the min() line is no longer needed, so delete it.
Missing type arguments for generic type "list": write list[Callable[..., object]] and list[str] again.
Function is missing a type annotation: def label(item: Item) -> str:. That also fixes the call from labels.
One way to solve it. Yours can look different and still pass the checks.
import annotationlib
from collections.abc import Callable
class Item:
def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line: str) -> Item:
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text: str) -> list[Item]:
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items: list[Item]) -> Item | None:
if not items:
return None
return min(items, key=lambda item: item.price)
def total(items: list[Item]) -> float:
return sum(item.price for item in items)
def summary(items: list[Item]) -> str:
best = cheapest(items)
if best is None:
return "no items"
return f"{len(items)} items, {total(items):.2f} in total, cheapest: {best.name}"
def signature_table(functions: list[Callable[..., object]]) -> list[str]:
rows: list[str] = []
for function in functions:
hints = annotationlib.get_annotations(function, format=annotationlib.Format.STRING)
returns = hints.pop("return")
params = ", ".join(f"{name}: {hint}" for name, hint in hints.items())
rows.append(f"{function.__name__}({params}) -> {returns}")
return rows
def label(item: Item) -> str:
return f"{item.name} ({item.price:.2f})"
def labels(items: list[Item]) -> list[str]:
return [label(item) for item in items]
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
import annotationlib
from collections.abc import Callable
class Item:
def __init__(self, name: str, price: float, tags: list[str] | None = None) -> None:
self.name = name
self.price = price
self.tags = tags or []
def parse_item(line: str) -> Item:
name, price = line.split(";")
return Item(name.strip(), float(price))
def load(text: str) -> list[Item]:
return [parse_item(line) for line in text.splitlines() if line.strip()]
def cheapest(items: list[Item]) -> Item | None:
if not items:
return None
return min(items, key=lambda item: item.price) # type: ignore
def total(items: list[Item]) -> float:
return sum(item.price for item in items)
def summary(items: list[Item]) -> str:
best = cheapest(items)
if best is None:
return "no items"
return f"{len(items)} items, {total(items):.2f} in total, cheapest: {best.name}"
def signature_table(functions: list) -> list:
rows: list[str] = []
for function in functions:
hints = annotationlib.get_annotations(function, format=annotationlib.Format.STRING)
returns = hints.pop("return")
params = ", ".join(f"{name}: {hint}" for name, hint in hints.items())
rows.append(f"{function.__name__}({params}) -> {returns}")
return rows
def label(item):
return f"{item.name} ({item.price:.2f})"
def labels(items: list[Item]) -> list[str]:
return [label(item) for item in items]
test_main.py
from main import labels, load, summary
def test_mypy_strict():
"""mypy --strict reports no errors for main.py"""
from mypy import api
report, errors, status = api.run(["--strict", "--no-error-summary", "main.py"])
assert status == 0, f"mypy found problems:\n{report}{errors}"
def test_labels():
"""labels gives the name and the price with two decimals"""
got = labels(load("pen; 1.50\nbook; 12\n"))
assert got == ["pen (1.50)", "book (12.00)"], f"labels returned {got!r}"
def test_summary_still_works():
"""summary is unchanged"""
assert summary([]) == "no items", "summary([]) should still return 'no items'"
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.pyfrom annotationlib import get_annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from decimal import Decimal
def total(amount: Decimal) -> Decimal:
return amount
print(get_annotations(total))
What Python prints
NameError: name 'Decimal' is not definedWhy, and the fix
Decimal exists only for the type checker, so evaluating the annotation fails. Ask for a format that does not need the name: get_annotations(total, format=Format.FORWARDREF) or format=Format.STRING. If your tool really needs the class, import it normally.
from __future__ import annotations
from annotationlib import get_annotations
def scale(n: int) -> int:
return n
converters = get_annotations(scale)
print(converters["n"]("3"))
What Python prints
TypeError: 'str' object is not callableWhy, and the fix
The __future__ import stores every annotation as a string, so converters["n"] is 'int', not the class. In Python 3.14 remove the import: annotations are lazy anyway. If it must stay, call get_annotations(scale, eval_str=True).
from typing import get_type_hints
def total(prices: List[float]) -> float:
return sum(prices)
print(total([1.5, 2.5]))
print(get_type_hints(total))
What Python prints
NameError: name 'List' is not definedWhy, and the fix
The def and the call work, because nothing reads the annotation. The error appears only when a tool evaluates it, possibly far away. Write list[float], or import List from typing, and run mypy, which reports Name "List" is not defined at the def line.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.