DMarlon Studio

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.

Python · Type hints, protocols and generics in DMarlon Studio: the example's code and its output in the terminal

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 anyway

Output · 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 Protocol accepts any class with the required methods.
  • def f[T](x: T) -> T declares 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.