Lesson 14 · 14 of 20
Type hints, protocols and generics · Python
Python type hints: aliases with type, typing.Protocol for structural typing and generic functions with the Python 3.12 syntax.

Type hints (title: str, -> Euros) document what each function expects and returns. Python does not check them at run time; editors and tools such as mypy or Pyright use them to flag mistakes before you run the program. type Euros = float creates an alias: a more meaningful name for an existing type.
A `Protocol` describes a shape, not a family: Priced says that any object with a price() method returning euros will do. Book and Subscription do not inherit from Priced and still satisfy it. This is structural typing (good old duck typing, but checkable): if it has price(), it fits.
That is why basket_total(items: list[Priced], ...) accepts books, subscriptions and any future class with price(). With `@runtime_checkable` you can also ask isinstance(basket[1], Priced) at run time: the subscription qualifies and the string "Dune" does not.
def first[T](items: list[T], default: T) -> T is a generic function (Python 3.12 syntax; older code uses TypeVar). T stands for any type, but the same one across the whole signature: given a list of strings, the checker knows the result is a string.
The last line makes the point: Subscription("3") passes a string where an int was expected and Python runs it without complaint. A type checker would flag it as an error.
Code · main.py
from typing import Protocol, runtime_checkabletype Euros = float # a type alias (Python 3.12)@runtime_checkableclass Priced(Protocol): # any class with price() fits def price(self) -> Euros: ...class Book: # no inheritance from Priced needed def __init__(self, title: str, cost: Euros) -> None: self.title = title self.cost = cost def price(self) -> Euros: return self.costclass Subscription: def __init__(self, months: int) -> None: self.months = months def price(self) -> Euros: return self.months * 7.99def basket_total(items: list[Priced], vat: float = 0.04) -> Euros: return round(sum(item.price() for item in items) * (1 + vat), 2)def first[T](items: list[T], default: T) -> T: # a generic function return items[0] if items else defaultbasket: list[Priced] = [Book("Dune", 11.5), Subscription(3)]print("Total:", basket_total(basket))print("Is Priced:", isinstance(basket[1], Priced), isinstance("Dune", Priced))print("First:", first(["INV-1", "INV-2"], "none"), "|", first([], 0))print("Not enforced:", repr(Subscription("3").months)) # runs anywayOutput · python3 main.py
Total: 36.89 Is Priced: True False First: INV-1 | 0 Not enforced: '3'
Key points
- Hints do not change what the program does: they document it.
type Name = ...creates a type alias.- A
Protocolaccepts any class with the required methods. def f[T](x: T) -> Tdeclares a generic function.- Run mypy or Pyright to make the hints catch bugs.
Exercise
Create a GiftCard class with price() and add it to basket without inheriting from anything; then write def last[T](items: list[T], default: T) -> T.