El mòdul 7 va acabar amb una botiga que ja no peta: s'explica. Però per explicar-se, Papyrus depenia de docstrings, de guard clauses i de la teva memòria: vendre() retorna la quantia o el Llibre? cercar_llibre() pot retornar None? Fins ara la resposta vivia al teu cap o en un comentari. Les anotacions de tipus (type hints) escriuen aquests contractes directament a la signatura de la funció, on l'editor, les eines i el proper lector — probablement tu d'aquí a tres mesos — poden veure'ls. En aquesta lliçó també saldem un deute concret: a la lliçó 05-06 vam dir que les anotacions de les dataclasses "són type hints que s'estudien al mòdul 8". Aquest moment és ara.

Contingut

  1. Què són els type hints i, sobretot, què NO fan
  2. Sintaxi bàsica: variables, paràmetres i valor de retorn
  3. Tipus genèrics moderns: list[Llibre], dict[str, Llibre], tuple, set
  4. | None i Optional: el contracte de cercar_llibre()
  5. Union amb |: quan una cosa pot ser diverses coses
  6. Anotar magatzem.py: les signatures reals de Papyrus
  7. TypedDict: donar forma a les files de CSV i JSON
  8. mypy: el verificador extern
  9. Beneficis reals i quan relaxar-se

Què són els type hints (i què NO fan)

Una anotació de tipus és una etiqueta que declara quin tipus de valor s'espera en una variable, un paràmetre o un retorn:

def preu_amb_iva(preu: float) -> float:
    return round(preu * 1.04, 2)
  • preu: float — el paràmetre hauria de ser un float.
  • -> float — la funció retorna un float.

I ara ve la cosa més important de tota la lliçó: Python no comprova les anotacions en temps d'execució. Aquest codi s'executa sense protestar:

preu_amb_iva("gratis")   # TypeError, si... pero pel *, no per l'anotacio
preu_amb_iva([1, 2, 3])  # les anotacions no han impedit la crida

L'error que veuràs és el mateix TypeError de sempre, produït en intentar multiplicar; l'anotació ni l'ha provocat ni l'ha evitat. Les anotacions són documentació estructurada: Python les desa (a __annotations__) però les ignora en executar. Qui les aprofita és l'ecosistema: el teu editor per autocompletar, i verificadors externs com mypy per detectar errors abans d'executar. Compara-ho amb el que feia el mòdul 7:

Mecanisme Quan actua Què fa si el tipus és incorrecte
Guard clause (raise TypeError) de 07-03 En execució Atura el programa amb un error accionable
__post_init__ de Llibre (05-06, 07-03) En execució, en crear l'objecte Rebutja la dada invàlida
Type hint Mai en execució Res — però mypy i l'editor avisen abans

No són alternatives: són capes. Els type hints detecten l'error a la teva pantalla mentre escrius; les guard clauses l'aturen si malgrat tot arriba en execució.

Sintaxi bàsica

Variables

IVA_LLIBRES: float = 0.04
DESCOMPTE_SOCI: float = 0.05
nom_botiga: str = "Papyrus"
unitats: int = 2
es_soci: bool = True

En variables locals amb valor evident (unitats = 2) l'anotació sol sobrar: el tipus s'infereix. On brilla és en constants de mòdul, en atributs i en variables que s'inicialitzen "buides":

titols_esgotats: list[str] = []   # sense l'anotacio, ningu no sap que contindra

Paràmetres i retorn

def preu_final(preu: float, soci: bool = False) -> float:
    """Aplica IVA i, si escau, el descompte de soci."""
    preu_iva = preu * (1 + IVA_LLIBRES)
    if soci:
        preu_iva *= (1 - DESCOMPTE_SOCI)
    return round(preu_iva, 2)

Fixa't que el valor per defecte conviu amb l'anotació: soci: bool = False. Una funció que no retorna res s'anota amb -> None:

def registrar_alta(codi: str) -> None:
    ...

Tipus genèrics moderns

Les col·leccions s'anoten indicant què contenen, amb claudàtors. Des de Python 3.9 es fan servir els tipus integrats directament (veuràs List i Dict importats de typing en codi antic; avui ja no calen):

from models import Llibre

cataleg: dict[str, Llibre] = {}          # titol → Llibre
pendents: list[str] = ["Faust"]          # llista de titols
venda: tuple[str, str, float] = ("2026-07-13", "Hamlet", 20.70)  # fila de vendes.csv
codis_soci: set[str] = {"LLUIS-001", "MARTA-002", "PAU-003"}
Anotació Es llegeix com Exemple a Papyrus
list[Llibre] llista d'objectes Llibre resultat de carregar el catàleg CSV
dict[str, Llibre] claus str, valors Llibre el catàleg en memòria
tuple[str, str, float] tupla d'exactament 3 elements amb aquests tipus una fila de vendes.csv
tuple[str, ...] tupla de longitud variable, tot str títols immutables
set[str] conjunt de str codis de soci vàlids

I aquí es salda la promesa de 05-06: a la dataclass Llibre, les línies titol: str, preu: float, estoc: int = 0 són exactament això — anotacions de tipus. @dataclass les llegeix de __annotations__ per generar __init__ i companyia. És l'únic lloc de Python on les anotacions tenen un efecte pràctic directe... i tot i així, en execució ningú no verifica que preu sigui de debò un float: per això Llibre necessita el seu __post_init__.

| None: el contracte de cercar_llibre()

Al mòdul 7 vam dissenyar dues funcions germanes amb contractes diferents: cercar_llibre() retorna el llibre o None (l'absència és un cas normal), i obtenir_llibre() retorna el llibre o llança LlibreNoTrobatError. Aquell contracte, que aleshores defensàvem a la docstring, ara s'escriu a la signatura:

def cercar_llibre(cataleg: dict[str, Llibre], titol: str) -> Llibre | None:
    """Retorna el Llibre o None si no es al cataleg."""
    return cataleg.get(titol)

Llibre | None es llegeix "un Llibre o None". És la forma moderna (Python 3.10+) d'Optional[Llibre], que encara veuràs en moltíssim codi:

from typing import Optional

def cercar_llibre(cataleg: dict[str, Llibre], titol: str) -> Optional[Llibre]: ...

Són equivalents. L'avantatge pràctic: quan escrius cercar_llibre(cataleg, "Hamlet").preu_final(), un verificador t'avisa que el resultat podria ser None i que None no té preu_final() — el clàssic AttributeError de les 20:05 amb la botiga tancada, detectat abans d'executar.

Union amb |

El mateix operador serveix per a qualsevol unió de tipus, no només amb None:

def carregar_config(clau: str) -> str | float | bool:
    """Retorna el valor de config.json, que pot ser text, numero o boolea."""
    ...

Fes-lo servir amb moderació: una funció que retorna str | float | bool | None està demanant a crits un redisseny. Els contractes clars de M7 (una funció, un tipus de resultat, o una excepció) continuen sent la millor guia.

Anotar magatzem.py: les signatures reals

Així queden les capçaleres de magatzem.py amb els contractes a la vista. Compara cada signatura amb el que la funció ja feia a M6 i M7 — no canvia ni una línia de lògica:

from pathlib import Path
from models import Llibre

BASE: Path = Path(__file__).parent

def cercar_llibre(cataleg: dict[str, Llibre], titol: str) -> Llibre | None: ...

def obtenir_llibre(cataleg: dict[str, Llibre], titol: str) -> Llibre:
    """Retorna el Llibre o aixeca LlibreNoTrobatError."""

def vendre(cataleg: dict[str, Llibre], titol: str, unitats: int) -> float:
    """Descompta estoc i retorna la quantia amb IVA (p. ex. Hamlet x2 → 20.70)."""

def reposar(cataleg: dict[str, Llibre], titol: str, unitats: int) -> None: ...

def carregar_cataleg(ruta: Path) -> dict[str, Llibre]: ...

def desar_cataleg(cataleg: dict[str, Llibre], ruta: Path) -> None: ...

def tancament_de_caixa(ruta_vendes: Path) -> float:
    """Suma les quanties del dia; salta files corruptes i les registra a WARNING."""

Observa quanta història del curs cap en una signatura: obtenir_llibre no anota Llibre | None perquè el seu contracte és llançar, no retornar None — l'excepció no forma part de l'anotació de retorn (es documenta a la docstring). I vendre retorna float: la quantia, no el llibre. Dubtes que a M7 resolies rellegint el cos, ara es responen sense obrir la funció.

TypedDict: forma per a les files de CSV i JSON

csv.DictReader (M6) retorna diccionaris, i dict[str, str] diu ben poc: quines claus? TypedDict declara la forma exacta:

from typing import TypedDict

class FilaVenda(TypedDict):
    data: str
    titol: str
    quantia: str   # DictReader sempre lliura str; convertir es cosa nostra (07-02)

def parsejar_fila(fila: FilaVenda) -> float:
    return float(fila["quantia"])

Amb això, fila["quanita"] (errada inclosa) deixa de ser una bomba de rellotgeria KeyError: mypy la marca a l'instant. És una pinzellada — suficient per a les files de vendes.csv i els objectes de cataleg.json; no et cal res més per ara.

mypy: el verificador extern

Si Python no comprova les anotacions, alguna cosa ho ha de fer. Aquesta cosa és mypy, que s'instal·la al venv del projecte com qualsevol paquet (M1):

pip install mypy
mypy magatzem.py

Exemple real del bug que caça. Aquest codi s'executa la majoria de vegades... fins que el llibre no existeix:

def preu_per_a_julia(cataleg: dict[str, Llibre], titol: str) -> float:
    llibre = cercar_llibre(cataleg, titol)
    return llibre.preu_final(soci=False)   # i si llibre es None?
magatzem.py:42: error: Item "None" of "Llibre | None" has no attribute
"preu_final"  [union-attr]
Found 1 error in 1 file (checked 1 source file)

mypy no ha executat res: ha llegit les signatures i ha deduït que aquell AttributeError era possible. L'arranjament és el patró LBYL/EAFP de 07-01, i ara l'eina t'obliga a triar-lo conscientment:

llibre = cercar_llibre(cataleg, titol)
if llibre is None:
    raise LlibreNoTrobatError(titol)
return llibre.preu_final(soci=False)

Després de l'if, mypy estreny el tipus: sap que llibre ja és Llibre. D'això se'n diu narrowing i és la raó per la qual anotar | None compensa sempre.

Beneficis i quan relaxar-se

Beneficis: documentació viva que no es desactualitza com els comentaris (si canvies la funció i no la signatura, mypy protesta); autocompletat precís a l'editor (escriu llibre. i apareixen preu_final i hi_ha_estoc); i bugs detectats abans d'executar, com el None de dalt.

Quan relaxar-se: en un script de vint línies per canviar el nom dels fitxers de copies/, anotar cada variable és burocràcia. La regla pràctica: anota sempre les signatures públiques dels teus mòduls (magatzem.py, models.py, errors.py — els seus atributs com demanat: int i disponible: int també agraeixen tipus); relaxa't en scripts d'usar i llençar i en variables locals òbvies. Els type hints són graduals per disseny: pots anotar un mòdul avui i un altre el mes que ve.

Errors Comuns i Consells

  • Creure que les anotacions validen: def vendre(unitats: int) no impedeix vendre("dos"). Per rebutjar dades en execució continuen calent les guard clauses de 07-03. Els hints avisen abans; els raise protegeixen durant.
  • Oblidar el | None en funcions que poden no trobar: si cercar_llibre s'anota -> Llibre, menteixes al lector i a mypy, que deixarà passar l'AttributeError. L'anotació ha de dir tota la veritat.
  • list a seques en comptes de list[Llibre]: és legal però perd gairebé tot el valor — l'editor no sabrà autocompletar els elements.
  • Importar List/Optional de typing per inèrcia: en Python modern (3.10+), list[...] i X | None són la forma preferida. Reconeix la vella quan la llegeixis; escriu la nova.
  • Consell: executa mypy com a part de la teva rutina, igual que mires papyrus.log. Un error de mypy és un bug que no ha arribat a producció.
  • Consell: quan una signatura anotada et quedi llarguíssima o plena de |, no et barallis amb la sintaxi — sol ser la signatura la que demana simplificar la funció.

Exercicis

  1. Anota completament aquesta funció de Papyrus (paràmetres i retorn), sabent que retorna el títol del llibre més car del catàleg o None si el catàleg és buit:

    def mes_car(cataleg):
        if not cataleg:
            return None
        return max(cataleg.values(), key=lambda l: l.preu).titol
    
  2. Defineix un TypedDict anomenat FilaCataleg per a les files del CSV del catàleg (claus titol, preu, estoc, totes str perquè venen de DictReader) i anota la funció fila_a_llibre(fila) que converteix una fila en un Llibre (pot llançar ValueError si el preu no és numèric — recorda: això va a la docstring, no al ->).

  3. Aquest codi passa desapercebut fins que l'Omar demana un títol esgotat. Quin error assenyalaria mypy i com el corregiries fent servir el contracte d'errors.py?

    def cobrar(cataleg: dict[str, Llibre], titol: str) -> float:
        llibre = cercar_llibre(cataleg, titol)
        if not llibre.hi_ha_estoc():
            raise EstocInsuficientError(titol, 1, 0)
        return llibre.preu_final()
    

Solucions

  1. def mes_car(cataleg: dict[str, Llibre]) -> str | None:
        if not cataleg:
            return None
        return max(cataleg.values(), key=lambda l: l.preu).titol
    

    El -> str | None és obligatori: hi ha un camí que retorna None. Anotar només -> str seria mentir.

  2. from typing import TypedDict
    
    class FilaCataleg(TypedDict):
        titol: str
        preu: str
        estoc: str
    
    def fila_a_llibre(fila: FilaCataleg) -> Llibre:
        """Converteix una fila de CSV en Llibre. Llanca ValueError si preu/estoc no son numerics."""
        return Llibre(fila["titol"], float(fila["preu"]), int(fila["estoc"]))
    

    Les conversions float()/int() són les mateixes que tancament_de_caixa protegeix amb el seu try mínim (07-02); l'excepció possible es documenta a la docstring.

  3. mypy assenyala Item "None" of "Llibre | None" has no attribute "hi_ha_estoc": cercar_llibre pot retornar None i None.hi_ha_estoc() petaria. Dos arranjaments coherents amb M7: comprovar if llibre is None: raise LlibreNoTrobatError(titol) abans de fer-lo servir, o directament cridar obtenir_llibre(cataleg, titol), la signatura del qual, -> Llibre, ja garanteix (llançant si cal) que hi ha llibre. La segona és millor: reutilitza el contracte existent.

Conclusió

Els type hints escriuen a les signatures els contractes que el mòdul 7 defensava a mà: cercar_llibre() -> Llibre | None diu tota la veritat, vendre() -> float esvaeix el dubte de la quantia, i les anotacions de la dataclass Llibre — la promesa pendent de 05-06 — per fi tenen nom i cognoms. Python no les comprova en execució; mypy i el teu editor les converteixen en una xarxa que caça bugs abans que la Júlia o l'Omar els pateixin. Amb els contractes ja visibles, el pas següent ataca una altra repetició: a Papyrus volem cronometrar tancament_de_caixa, auditar cada venda al log de M7 i reintentar operacions fràgils — sense copiar i enganxar el mateix codi al voltant de cada funció. Això és exactament el que resolen els decoradors, i són la propera lliçó.

Curs de Programació en Python

Mòdul 1: Introducció a Python

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats